Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

データサイエンスチームYAMALEXでGPT関連の発表をしてきました(ChatGPT Meetup/JJUGナイトセミナー)

こんにちは。
Acroquestのデータサイエンスチーム「YAMALEX」に所属する@shin0higuchiです😊
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

先週5/23(火)、5/25(木)に、
佐々木(@Ssk1029Takashi )と 私でそれぞれGPT関連の発表をおこないました。
簡単に発表の概要をご紹介します。

JJUGナイトセミナー (5/23)

私はJJUG(日本Javaユーザー会)が主催する「JJUGナイトセミナー『AI × Java 祭』」で発表をおこないました。
jjug.doorkeeper.jp


JJUGナイトセミナーはオンラインで月に1回程度開催されており、Javaの最新情報や事例などが発表される勉強会です。
今回は「AI × Java 祭」ということで、主にAIによるコードアシストにフォーカスした内容となっていました。

私の発表内容としては、ChatGPTを提供しているOpenAIやそのAPIの概要を説明したうえで、実際の使い方の例を紹介しました。また、Java向けのクライアントを利用してAPIを呼び出すデモを実施しました。
タスク判定や文章要約など、様々なことに利用可能ですので、興味がある方は是非ご覧ください。

発表資料

speakerdeck.com

イベント動画

www.youtube.com

ChatGPT Meetup (5/25)

こちらはChatGPTのユーザーコミュニティによる勉強会で、GPT/ChatGPTについて、10以上のLT発表がありました。
GPT活用における様々なTipsが得られる濃い内容だったと思います。
佐々木は、「Semantic Kernelを使ってGPTと外部ツールを簡単に連携する」というテーマで発表をおこないました。

chatgpt.connpass.com

Semantic Kernelは、Microsoftが公開しているOSSで、LLMを活用したアプリケーションを開発するためのSDKとなっています。
コンテキストを保持することで連続した会話を扱ったり、スキルと呼ばれる単位でLLMに渡したいプロンプトを管理したりすることが可能です。
また、プランナーと呼ばれる機能によって、処理の実行計画を作り、ステップごとに処理を進めることも可能です。

アプリケーションに組み込むうえで非常に便利なソフトウェアと言えると思います。
興味があれば、是非発表資料をご覧ください。

発表資料

speakerdeck.com

まとめ

大規模言語モデルの発達が様々な領域でのイノベーションに繋がっており、今後も目が離せないですね。
引き続き最新情報へのキャッチアップ/発信をして行きたいと思います。

今回の記事は以上となります。お読みいただきありがとうございました。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Azure OpenAI Serviceを活用したいエンジニア募集! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

SpringBoot/Quarkus/Micronautの性能を検証してみた ~その1 起動編~

こんにちは。@phonypianistです。
本投稿はアクロクエスアドベントカレンダー 12月21日 の記事です。

最近、Quarkusアプリを本番適用しました。
QuarkusはJavaアプリを作るための軽量なフレームワークで起動が速いって聞くけど、実際どれくらい速いんだろう?と気になったので、Spring Bootや、類似OSSのMicronautと比べてみました。

背景

JavaフレームワークといえばSpringBootが主流ですが、起動が遅かったり、必要なメモリが多かったりしています。 これは、アプリ起動時にリフレクションを用いてDI(Dependency Injection)を行っているのが要因の1つです。

マイクロサービス、コンテナネイティブなアプリケーションは、負荷の状況に応じて、シームレスにスケールアウトできる必要があります。 アプリケーションの起動速度が遅かったり、メモリ消費量が多かったりすると、負荷に対してシステムの性能が追随できません。

この問題を解決し高速化するために、QuarkusやMicronautといった新しいフレームワークが登場しています。

ja.quarkus.io

micronaut.io

QuarkusもMicronautも、コンパイル時にDIの解決を行うことで、起動速度や処理速度の向上、およびメモリ使用量の削減ができます。

簡単に、SpringBoot、Quarkus、Micronautの特徴を以下にまとめます。

フレームワーク 特徴
SpringBoot
Quarkus
Micronaut
  • メモリ使用量が少なく、起動時間が短くなるようにしたJavaフレームワーク
  • SpringBootと一部互換性を持っており、SpringBootから移行しやすいように作られている

QuarkusやMicronautでは、GraalVMを使用してネイティブアプリも作ることができます。 これにより、アプリの起動時間を飛躍的に短縮し、メモリも節約することができます。
※Spring Bootもネイティブアプリが作れるようになりましたが、いくつか課題があるため今回は対象外としています。

今回は、アプリの起動における性能について、SpringBoot、Quarkus、Micronautでどれくらい異なるのか、調べてみました。 QuarkusとMicronautは、ネイティブアプリも計測してみました。

前提

環境構成

AWS Fargateを用いて、以下のアプリを起動します(それぞれ1コンテナずつ、計5コンテナを起動)。

  1. SpringBoot (Java)
  2. Quarkus (Java)
  3. Quarkus (Native)
  4. Micronaut (Java)
  5. Micronaut (Native)

性能比較構成

アプリの1コンテナあたりのリソースは、1vCPU、2GBメモリを割り当てました。

検証に使用するアプリ

データベースのテーブルにあらかじめ入っているレコードを取得・返却するアプリです。 REST APIを提供し、クライアントからそのAPIが呼び出されると、JPAを用いてデータベースからレコードを取得し、クライアントに返却します。 SpringBoot、Quarkus、Micronautそれぞれ同じ処理内容にしてあります。

使用するミドルウェア・ライブラリのバージョンは以下の通りです。

  • Java:17
  • SpringBoot:3.0.0
  • Quarkus:2.13.1.Final
  • Micronaut:3.7.1

それぞれのソースコードは以下の通りです。

SpringBoot

UserController.java

@RestController
@RequestMapping("user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    Iterable<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/{userId}")
    User getUser(@PathVariable("userId") String userId) {
        return userService.getUser(userId);
    }

}

UserService.java

@Component
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public Iterable<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User getUser(String userId) {
        return userRepository.findById(userId).orElseThrow(() -> new RuntimeException(userId));
    }

}

UserRepository.java

@Repository
public interface UserRepository extends CrudRepository<User, String> {

}

Quarkus

UserResource.java

@Path("/user")
@Produces("application/json")
public class UserResource {

    @Inject
    UserService userService;

    @GET
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GET
    @Path("/{userId}")
    public User getUser(@PathParam("userId") String userId) {
        return userService.getUser(userId);
    }

}

UserService.java

@ApplicationScoped
public class UserService {

    @Inject
    UserRepository userRepository;

    public List<User> getAllUsers() {
        return userRepository.findAllUsers();
    }

    public User getUser(String userId) {
        return userRepository.findByUserId(userId);
    }

}

UserReposirory.java

@ApplicationScoped
public class UserRepository implements PanacheRepository<User> {

    public List<User> findAllUsers() {
        return findAll().list();
    }

    public User findByUserId(String userId) {
        return find("user_id", userId).firstResult();
    }

}

Micronaut

UserController.java

@Controller("/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @Get
    Iterable<User> getUsers() {
        return userService.getUsers();
    }

    @Get("/{userId}")
    User getUser(@PathVariable("userId") String userId) {
        return userService.getUser(userId);
    }

}

UserService.java

@Singleton
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public Iterable<User> getUsers() {
        return userRepository.findAll();
    }

    public User getUser(String userId) {
        return userRepository.findById(userId).orElseThrow(() -> new RuntimeException(userId));
    }

}

UserRepository.java

@Repository
public interface UserRepository extends CrudRepository<User, String> {

}

計測内容

5つのアプリを起動して、それぞれの起動速度(起動にかかった時間)と、メモリ使用量を計測します。

起動にかかった時間は、CloudWatchログに出力される、アプリ(各フレームワーク)の起動時間を確認します。 メモリ使用量は、CloudWatchメトリクスの値を確認します。

1つのアプリに対して5回起動し、最大と最小を除く3回分の、起動にかかった時間とメモリ使用量の平均値を計算しました。

計測結果

起動完了までの時間は、以下のようになりました。

フレームワーク別起動完了までの時間

起動完了までの時間は、やはりQuarkusもMicronautも、ネイティブアプリが爆速です。
Javaアプリの中では、Quarkusが他のフレームワークと比べると速くなっています。

続いて、起動直後のメモリ使用量です。

起動直後のメモリ使用量

メモリ使用量も、やはりネイティブアプリがJavaアプリの半分の量の消費で抑えられています。特にQuarkusのネイティブアプリはたった12MBの消費で抑えられています。
Javaアプリでは、SpringBootよりQuarkusやMicronautの方が、少し消費量が低くなっています。

まとめ

やはり、ネイティブアプリは起動時間もメモリ使用量もパフォーマンスが良いですね。 Javaアプリの中では、Quarkusが他のフレームワークよりもパフォーマンスが良いようです。
※アプリの構成や環境によって、変わることがあります。

今回は、起動時のパフォーマンスについて検証しました。
次回は、リクエスト処理性能について検証したいと思います。

それでは。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

Spring Modulith でモジュラモノリスなアプリの構造を検証してみた

アクロクエスアドベントカレンダー 12月9日 の記事です。

普段は Java, Python でバックエンドの開発をしている大塚優斗です😃

最近は Spring フレームワークのメジャーアップデートなどで盛り上がっていますね!

10月にこんな記事を見かけて、Spring Modulith がとても気になっていたので、手元で試したことを書いていきます✍️

Spring Modulith とは

Spring Modulith はモジュラモノリスな Spring Boot アプリケーションの開発をサポートするプロジェクトです。

元々は Moduliths というプロジェクトでしたが、現在はプロジェクト名が Spring Modulith に変更され、Spring Boot 3 をベースラインとした実験的なプロジェクトとなっています。

spring.io

モジュラモノリスは、モノリスのように単一でデプロイ可能な形を保ちつつ、マイクロサービスのようにモジュール性を持ったアーキテクチャです。
モジュラモノリスの説明は、日本語でとてもわかりやすい記事が書かれているので、そちらを参照してください。

モジュラモノリスに移行する理由 ─ マイクロサービスの自律性とモノリスの一貫性を両立させるアソビューの取り組み - エンジニアHub|Webエンジニアのキャリアを考える!

モジュラモノリスでは、自己完結したビジネスドメインに合わせた疎結合なモジュールを作ることが重要と言われています。
そうすることによって、モジュールごとに独立して開発することが容易になり、あるビジネスドメインで仕様の変更が起きたとしても、影響範囲を対応するモジュールに留めることができるなどのメリットがあるためです。

しかし、単一のリポジトリで開発するため、やろうと思えば各モジュールを密結合にすることができてしまいます。
例えば、Java では子パッケージのクラスを参照するには、子パッケージのクラスに public 修飾子をつける必要がありますが、これによって他モジュールのクラスからアクセスできるようになってしまいます。 さらに、コンパイラはこれらの意図しない参照に対して、警告してくれません。

Spring Modulith では、各モジュールを疎結合に保つためにモジュールの境界ルールを定め、それをテストとして扱うことで、開発者が良くモジュール化されたコードを実装することをサポートしてくれます。

Spring Modulith でできること

公式ドキュメントによると、主に下記のようなことができるようです。

  1. モジュール構造の検証
  2. モジュールに閉じた結合テスト
  3. イベントによるモジュール同士の連携
  4. モジュールのドキュメント化
  5. モジュール同士の連携のモニタリング

この記事では、5番目以外の項目を試していきたいと思います。

この記事内で扱うサンプルコードでは、Maven 3, Java 17, Spring Boot 3.0.0 を使用しています。

0. Spring Modulith でのパッケージの扱いについて

各機能を見ていく前に Spring Modulith におけるパッケージの扱いについて、説明しておきます。

Spring Modulith では、メインアプリケーションクラスが存在するパッケージ直下のサブパッケージをアプリケーションモジュールと呼び、あるドメインに対応するモジュールとして扱います。
そして、アプリケーションモジュール直下の public なクラスは、そのモジュールの公開インターフェースとして扱われます。
また、アプリケーションモジュール配下のサブパッケージは、他のモジュールからアクセスされない内部的なものとして扱われます。

後述しますが、Spring Modulith ではモジュール構造にいくつかのルールが決められており、モジュール構造の検証テストを実行した際に、決まっているルールに違反した場合、テストが失敗します。

この記事内のサンプルコードでは、公式ドキュメントと同様に Order モジュールと Inventory モジュールを扱います。
ファイル構成は以下の通りです。

src/main/java/com/example
└── samplemodulith
    ├── SampleModulithApplication.java
    ├── inventory  # アプリケーションモジュール①
    │   ├── InventoryService.java  # アプリケーションモジュール直下のファイルは公開インターフェースとして扱われる
    │   └── internal  # 他モジュールからはアクセスされないカプセル化されたパッケージ
    │       └── InternalInventoryComponent.java
    └── order  # アプリケーションモジュール②
        ├── Order.java
        ├── OrderCompleted.java
        └── OrderService.java
src/test/java/com/example
└── samplemodulith
    ├── DocumentationTests.java
    ├── ModularityTests.java
    ├── SampleModulithApplicationTests.java
    ├── inventory
    │   └── InventoryIntegrationTests.java
    └── order
        └── OrderIntegrationTests.java
pom.xml

サンプルコードのパッケージ構造

1. モジュール構造の検証

Spring Modulith によるアプリケーションモジュールの検証では以下の3つのことができ、それぞれどういった場合にテストが失敗するかというルールがあります。

  1. 循環参照の検知
    • アプリケーションモジュール間で循環参照している場合にテストが失敗する
  2. 別モジュールへのアクセス違反の検知
    • あるアプリケーションモジュールが別のアプリケーションモジュールの内部パッケージを参照した場合にテストが失敗する
  3. 依存するアプリケーションモジュールの明示
    • 明示的にモジュールの依存関係を定義したときに他のアプリケーションモジュールへの依存があった場合にテストが失敗する

3 はオプション的な立ち位置なので、基本的な 1 と 2 をそれぞれ試していきます。

循環参照の検知

モジュール間に循環参照があった場合にテストで検知できることを試していきます。

サンプルコードでは、循環参照を作るためにコンストラクタで OrderService クラスと InventoryService クラスが相互依存するように定義しています。

package com.example.samplemodulith.order;

import com.example.samplemodulith.inventory.InventoryService;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final InventoryService inventoryService;

    public OrderService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }
}
package com.example.samplemodulith.inventory;

import com.example.samplemodulith.order.OrderService;
import org.springframework.stereotype.Service;

@Service
public class InventoryService {

    private final OrderService orderService;

    public InventoryService(OrderService orderService) {
        this.orderService = orderService;
    }
}

モジュール間で循環参照がある

モジュール構造の検証は ApplicationModules インスタンスverify メソッドで行えます。

package com.example.samplemodulith;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.model.ApplicationModules;

public class ModularityTests {

    @Test
    void verifyModularity() {
        var modules = ApplicationModules.of(SampleModulithApplication.class);
        modules.forEach(System.out::println);
        modules.verify();
    }
}

テストを実行すると、以下のようにモジュール同士が循環参照していることをわかりやすく表示してくれました。

org.springframework.modulith.model.Violations: - Cycle detected: Slice inventory -> 
                Slice order -> 
                Slice inventory
  1. Dependencies of Slice inventory
    - Constructor <com.example.samplemodulith.inventory.InventoryService.<init>(com.example.samplemodulith.order.OrderService, com.example.samplemodulith.inventory.internal.InternalInventoryComponent)> has parameter of type <com.example.samplemodulith.order.OrderService> in (InventoryService.java:0)
    - Field <com.example.samplemodulith.inventory.InventoryService.orderService> has type <com.example.samplemodulith.order.OrderService> in (InventoryService.java:0)
  2. Dependencies of Slice order
    - Constructor <com.example.samplemodulith.order.OrderService.<init>(com.example.samplemodulith.inventory.InventoryService, org.springframework.context.ApplicationEventPublisher)> has parameter of type <com.example.samplemodulith.inventory.InventoryService> in (OrderService.java:0)
    - Field <com.example.samplemodulith.order.OrderService.inventoryService> has type <com.example.samplemodulith.inventory.InventoryService> in (OrderService.java:0)

別モジュールへのアクセス違反の検知

次は、あるモジュールが他モジュールの内部パッケージにアクセスしている場合に、テストで検知されることを確認してみます。

Order モジュールが Inventory モジュールの内部パッケージにあるクラスに依存するコードを用意します。

package com.example.samplemodulith.order;

import com.example.samplemodulith.inventory.internal.InternalInventoryComponent;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final InternalInventoryComponent internalInventoryComponent;

    public OrderService(InternalInventoryComponent internalInventoryComponent) {
        this.internalInventoryComponent = internalInventoryComponent;
    }

OrderモジュールのクラスがInventoryモジュールの内部パッケージにアクセスしている

先ほどと同じテストを実行すると、以下のようにエラーになりました。

org.springframework.modulith.model.Violations: - Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
OrderService declares constructor OrderService(InternalInventoryComponent, ApplicationEventPublisher) in (OrderService.java:0)
- Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
Field <com.example.samplemodulith.order.OrderService.internalInventoryComponent> has type <com.example.samplemodulith.inventory.internal.InternalInventoryComponent> in (OrderService.java:0)
- Module 'order' depends on non-exposed type com.example.samplemodulith.inventory.internal.InternalInventoryComponent within module 'inventory'!
Constructor <com.example.samplemodulith.order.OrderService.<init>(com.example.samplemodulith.inventory.internal.InternalInventoryComponent, org.springframework.context.ApplicationEventPublisher)> has parameter of type <com.example.samplemodulith.inventory.internal.InternalInventoryComponent> in (OrderService.java:0)

内部パッケージの中には、同一モジュールの公開インタフェースからの参照があるために public になるクラスもありますが、これらのクラスが別モジュールからアクセスされた場合に、テストで検知できるのはとても助かりますね。

2. モジュールに閉じた結合テスト

この章では、以下の2点を確認していきます。

  1. 単一のアプリケーションモジュールで結合テストができること
  2. Bootstrap モードによって、結合テスト時に他モジュールの Bean 生成ができること

単一のアプリケーションモジュールで結合テストができること

Spring Modulith では、ある単一のモジュールの結合テストを独立して実行できます。
これにより不要な Bean 生成をせずに済みます。

Spring Modulith では、 Spring Boot ではお馴染みの @SpringBootTest アノテーションの代わりに、@ApplicationModuleTest アノテーションを用いて、テストを実行します。

package com.example.samplemodulith.inventory;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;

@ApplicationModuleTest
public class InventoryIntegrationTests {

    @Autowired
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}

テスト実行後、以下のようなログが出力されます。 STANDALONE モードで起動され、他モジュールの依存性がないことがわかりますね。
他モジュールへの依存があった場合は、テストが失敗します。

Bootstrapping @ModuleTest for inventory in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)========================================================================================================================
## inventory ##
> Logical name: inventory
> Base package: com.example.samplemodulith.inventory
> Direct module dependencies: none
> Spring beans:
  + ….InventoryService
  + ….internal.InternalInventoryComponent

Bootstrap モードによって、結合テスト時に他モジュールの Bean 生成ができること

次に、あるモジュールが他のモジュールの Bean に依存するパターンを見ていきます。

OrderモジュールがInventoryモジュールに依存している

この場合、2つのモジュール内で定義されている Bean を生成する必要がありますが、Spring Modulith には直接依存しているモジュールの Bean も同時に Bootstrap する DIRECT_DEPENDENCIES モードがあります。

@ApplicationModuleTest(mode = ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
class OrderIntegrationTests {

    @Autowired
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}

実行後のログは以下のようになります。

Order モジュールが Inventory モジュールに依存しており、Order モジュールの結合テストでは InventoryService クラスが注入されていることを確認できます。

Bootstrapping @ModuleTest for order in mode DIRECT_DEPENDENCIES (class com.example.samplemodulith.SampleModulithApplication)=============================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: inventory
> Spring beans:
  + ….OrderService
=============================================================================================================================
Included dependencies:
=============================================================================================================================
## inventory ##
> Logical name: inventory
> Base package: com.example.samplemodulith.inventory
> Direct module dependencies: none
> Spring beans:
  + ….InventoryService
  + ….internal.InternalInventoryComponent
=============================================================================================================================

ただし、依存先のモジュールが利用できない場合でもテストができるように、依存先モジュールの Bean をモックする方がベターです。

従来通り Spring Boot の @MockBean アノテーションを使用します。

@ApplicationModuleTest
class OrderIntegrationTests {

    @MockBean
    private InventoryService inventoryService;

    @Test
    void test() {
    }
}
Bootstrapping @ModuleTest for order in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)====================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: inventory
> Spring beans:
  + ….OrderService

Order モジュールだけでテストが実行できていることが確認できました。

3. イベントによるモジュール同士の連携

よりモジュール同士を疎結合にするために、Spring Modulith ではモジュール間のやりとりに Spring Application Events を使うことを推奨しています。

モジュール間のやり取りにイベントを用いることで、呼び出し元が呼び出し先について知る必要がなくなり、テスト時も呼び出し先の Spring Bean に依存/モックする必要がなくなります。

Kafka などのメッセージングシステムを使用することに似ていますが、Spring Application Events は Spring Framework が提供しているため、追加の依存関係/インフラは必要ありません。

OrderService の実装は以下のようになります。InventoryService に依存しない形になりました。

package com.example.samplemodulith.order;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final ApplicationEventPublisher events;

    public OrderService(ApplicationEventPublisher events) {
        this.events = events;
    }

    public void complete() {
        events.publishEvent(new OrderCompleted());
    }
}
package com.example.samplemodulith.order;

public class OrderCompleted {
}

InventoryServiceはOrderCompletedイベントをlistenする

テストでは、@ApplicationModuleTest アノテーションによって、テストメソッドに PublishEvents インスタンスを注入することができます。

ログからも他のモジュールに依存しない形でテストできていることがわかります。

import org.springframework.modulith.test.PublishedEvents;

import static org.assertj.core.api.Assertions.assertThat;

@ApplicationModuleTest
class OrderIntegrationTests {

    @Autowired
    private OrderService orderService;

    @Test
    void complete_success(PublishedEvents events) {
        orderService.complete();

        assertThat(events.ofType(OrderCompleted.class)).hasSize(1);
    }
}
Bootstrapping @ModuleTest for order in mode STANDALONE (class com.example.samplemodulith.SampleModulithApplication)====================================================================================================================
## order ##
> Logical name: order
> Base package: com.example.samplemodulith.order
> Direct module dependencies: none
> Spring beans:
  + ….OrderService

4. モジュールのドキュメント化

SpringModulith では、モジュール間の関係を表すモジュールコンポーネント図と、モジュールキャンバスと呼ばれるモジュールの概要表を生成できます。

ドキュメントの生成には、ApplicationModules を使用し、以下のテストコードでモジュールコンポーネント図とモジュールキャンバスを作成しています。

package com.example.samplemodulith;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.docs.Documenter;
import org.springframework.modulith.model.ApplicationModules;

public class DocumentationTests {

    ApplicationModules modules = ApplicationModules.of(SampleModulithApplication.class);

    @Test
    void writeDocumentationSnippets() {
        new Documenter(modules)
                .writeModulesAsPlantUml()
                .writeIndividualModulesAsPlantUml()
                .writeModuleCanvases();
    }
}

テスト実行後、target ディレクトリ配下に以下のような形で生成されたドキュメントが置かれます。

target/spring-modulith-docs
├── components.uml
├── module-beanreference.adoc
├── module-beanreference.uml
├── module-catalog.adoc
├── module-catalog.uml
├── module-inventory.adoc
├── module-inventory.uml
├── module-order.adoc
├── module-order.uml
├── module-typereference.adoc
└── module-typereference.uml

モジュールコンポーネント

コンポーネント図のスタイルには C4 か UML を選択できます。
図中でモジュール間の依存関係を表している矢印は以下の3つに分かれます。

  1. depends on
    • 他モジュールの型を参照している
  2. uses
    • 他モジュールのSpring Beanを参照している
  3. listen to
    • 他モジュールのイベントを待ち受けている

モジュールコンポーネント図の例。説明のためにtypereferenceモジュールとbeanreferenceモジュールを追加しています

モジュールキャンバス

Spring Modulith では、モジュールの概要を表形式でまとめたものをモジュールキャンバスと呼んでいます。
モジュールキャンバスは、以下のセクションに分かれており、Asciidoc ファイルで生成されます。

  1. アプリケーションモジュールのベースパッケージ名
  2. モジュールで公開されている Spring Beans
  3. jMolecules によって集約として扱われている集約ルート
  4. モジュールによって publish されるイベント
  5. モジュールによって listen されるイベント
  6. モジュールで公開されている Spring Boot Configuration properties

試しに Inventory モジュールのモジュールキャンバスを見てみると以下のようになっていました。

Inventoryモジュールのキャンバス

より多くのセクションが含まれている表の例が、公式ドキュメントに書かれているので、そちらも参照してください。

まとめ

今回は、Spring Modulith の基本的な使い方を手を動かしながら試してみました。

モジュラモノリスでアプリを作る際に、モジュール構成のテストをすることで、モジュールを疎結合に保ちながら開発できるのは心強いと思いました。

ちなみに、Spring Modulith 開発者の Oliver Drotbohm さんによると、2023年の第二四半期に非実験版のプロジェクトに昇格させる予定のようなので楽しみですね!

また、この記事で紹介できていない機能もあるので、興味がある方はぜひご自身で調べてみてください。

参考

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

LambdaのSnapStartをSpring Cloud Functionで検証してみた

3歳の息子がきらきら星を歌っていたので、きらきら星変奏曲モーツァルト)を弾いたら、「それは違う」と否定されてしまった@phonypianistです。

AWS re:Invent 2022でLambdaのSnapStartが発表されました。サーバーレス界隈に衝撃が走っていますw
ということで、本ブログのアドベントカレンダー12/8の記事として、SnapStartを紹介します。

Lambda SnapStartとは

LambdaのInitフェーズに時間がかかる問題、いわゆるコールドスタート問題を解決するための機能です。

aws.amazon.com

Lambdaのライフサイクルは大まかには、「初期化 (Init)」「起動 (Invoke)」「シャットダウン (Shutdown)」の3つのフェーズに分かれています。

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle より引用

最初の呼び出し、もしくはしばらく時間が経過したりスケールアウトしたりする際に「初期化」が行われますが、この部分はプログラムを実行するための環境準備やライブラリ読み込みなど、通常重い処理が行われます。 とりわけ、JavaではJava VMの起動があり、PythonやNode.jsに比べると「初期化」フェーズに時間がかかります。

SnapStartでは、Lambdaが実行されたときの状態のスナップショットを取っておき、それを復元することで、「初期化」フェーズの高速化を行います。 これにより、JavaではJava VMの起動が省略され、圧倒的に速くLambdaを起動することができるようになります。

実際にどれくらい高速になるのか、起動に時間がかかるSpringアプリケーション(Spring Cloud Function)を使って試してみました。 また、スナップショットのサイズはLambdaのメモリサイズに依存すると思われるため、Lambdaのメモリサイズにも影響するか検証してみました。

検証の概要

Spring Cloud Functionで作ったエコー(送られてきた文字をそのまま返す)アプリケーションをLambda上で動かし、CloudShellからリクエストを送って性能を検証します。

SnapStart検証構成

コールドスタートを確実に発生させるために、アプリケーションではリクエストを受けたときに1秒スリープさせます。

準備

プロジェクトの生成

Spring Cloud Functionのコードを用意します。 Spring Initializrで依存関係に「Spring Cloud Function」を追加してプロジェクトを生成します。今回はMavenプロジェクトにしました。 注意点として、LambdaはJava 11しか対応していないため、最新のSpring Boot 3は選択できません。Spring Boot 2とJava 11を選択してください。

pom.xmlの修正

生成したプロジェクトにはAWS用のライブラリがないため、pom.xmlにspring-cloud-function-adapter-awsを追加します。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>

Spring Bootが生成するJARファイルはそのままではLambdaから呼び出せないため、maven-shade-pluginでJARファイルを生成するように設定します。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot.experimental</groupId>
          <artifactId>spring-boot-thin-layout</artifactId>
          <version>1.0.28.RELEASE</version>
        </dependency>
      </dependencies>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>3.0.0</version>
        </dependency>
      </dependencies>
      <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
        <shadedArtifactAttached>true</shadedArtifactAttached>
        <shadedClassifierName>aws</shadedClassifierName>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.handlers</resource>
          </transformer>
          <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
            <resource>META-INF/spring.factories</resource>
          </transformer>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.schemas</resource>
          </transformer>
        </transformers>
      </configuration>
    </plugin>
  </plugins>
</build>

これにより、 mvn clean package を実行することで、Lambdaで設定可能な snapstartdemo-0.0.1-SNAPSHOT-aws.jar ファイルが生成できます。

Javaコードの実装

簡易的に実装します。 メインクラスに echo メソッドを実装し、1秒待つ処理を追加します。

@SpringBootApplication
public class SnapStartDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LambdademoApplication.class, args);
    }

    @Bean
    public Function<Flux<String>, Flux<String>> echo() {
        return flux -> flux.doOnNext(this::sleep);
    }

    private void sleep(String text) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Do nothing.
        }
    }

}

serverless.yml

今回はServerless Frameworkでデプロイします。 512MB/1024MB/2048MB/4096MBの4種類のメモリサイズ×SnapStartのON/OFFの組み合わせで計8個のLambda関数を作成します。 アプリケーションはすべて同じとします。

SnapStartを有効にするために、resources.extensionsでSnapStartのApplyOnに PublishedVersions を指定します。

また、SnapStartはLambdaのバージョン指定での実行が必要です。Serverless FrameworkのデフォルトではAPI Gatewayから呼ばれるLambdaのバージョンは $LATEST になり、SnapStartのオプションを有効にしていてもSnapStartが機能しません。そのため、serverless-plugin-canary-deploymentsプラグインを用いて、API GatewayからLambdaを呼び出す際にLambdaバージョンが指定されるようにします。

service: snapstartdemo

provider:
  name: aws
  stage: dev
  region: ap-northeast-1
  runtime: java11
  timeout: 15
  logRetentionInDays: 7
  iamRoleStatements:
    - Effect: Allow
      Action:
        - codedeploy:*
      Resource:
        - '*'

package:
  # mvn clean packageで生成される、AWS用のjarファイルを指定する
  artifact: target/snapstartdemo-0.0.1-SNAPSHOT-aws.jar

functions:

  # これ以降は、SnapStart有効/無効の順番に、メモリサイズが異なるLambda関数を定義する
  # SnapStart有効/無効の設定は、下のresourcesに定義する

  SpringCloudFunction512MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction512MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

resources:
  - extensions:
      # SnapStartを有効にする設定を行う(デフォルトは無効)
      SpringCloudFunction512MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction1024MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction2048MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction4096MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions

plugins:
  # Lambdaのバージョンを設定するためのプラグインを指定する
  - serverless-plugin-canary-deployments

デプロイ

deployコマンドでデプロイします。

serverless deploy

検証方法

クラスメソッドさんの以下の記事でまとまっていますので、こちらを参考にさせていただきました。 CloudShellから、API Gatewayに対して ab コマンドで100並列でリクエストを送ります。

dev.classmethod.jp

検証結果

Lambdaに割り当てたメモリサイズ毎に、SnapStartの無効/有効時のコールドスタートにかかった時間は、以下のようになりました。

メモリサイズ毎のコールドスタートにかかった時間(最小/最大/平均)
 SnapStart無効SnapStart有効
メモリサイズ最小最大平均最小最大平均
512MB4278.455049.414737.08195.01416.18285.83
1024MB4534.515200.474764.89267.76398.31326.71
2048MB3721.234659.764161.39203.59813.66321.07
4196MB3084.843607.293222.51212.74768.53353.76
メモリサイズ毎のコールドスタートにかかった時間(パーセンタイル)
 SnapStart無効SnapStart有効
メモリサイズp50p90p99p50p90p99
512MB4717.684881.765049.41283.59359.71416.18
1024MB4745.574906.425200.47321.88373.49398.31
2048MB4132.394345.264659.76345.51406.48813.66
4196MB3213.913354.723607.29354.87424.72768.53

90パーセンタイル値をグラフにすると、以下のようになりました。

割り当てメモリ毎のコールドスタート時間

どのメモリサイズでも、SnapStart有効の場合はSnapStart無効の場合に比べて約10倍も時間が短縮しています。

なお、Lambdaのメモリサイズを大きくすればするほど、SnapStartなしでの起動時間は短くなっていますが、SnapStart有効の場合はわずかに長くなっています。これはおそらく、スナップショットのサイズがメモリサイズに依存して大きくなっているものと思われますが、気にするほどではなさそうです。

まとめ

SnapStartを有効にすることで、JavaのLambdaコールドスタートの時間を大幅に短縮することができました。 今後はLambda関数をJavaで実装する、という選択もありかと思います。

ただ、X-Rayが使用できない、512MBより大きなエフェメラルストレージが使用できない等の制限があるため、注意が必要。 加えて、LambdaではJava 11(Amazon Corretto 11)しか選択できないのが残念なところ。 早くJava 17に対応してもらえないかなー。

では。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

Deep Java Library(DJL)を使ってみた

hayakawaです。
今回はJavaディープラーニングを実装できるOSSであるDeep Java Library(DJL)を使ってみました。

https://djl.ai/


ディープラーニングで何かをやるとしたら、現状ではPythonで開発するケースが多いですが、
システム全体としてはJavaで開発をしたいが、ディープラーニングの処理を利用したい場合、
これまでだと良いソリューションがなく、別サービスとしてAPI呼び出しをしたり、
JavaからPythonのプロセスを呼び出したりするようなことが必要でした。
ただこれだと、パフォーマンスが求められる場合、なかなか厳しいものがあります。

このOSSはそのようなケースで、Java上でのディープラーニング処理も実行できるようにすることを目指しているようです。

aws.amazon.com

Pythonで学習・モデル作成を行い、それをJavaで推論する、ということもできるようですが、
今回は、基本的な判定処理と学習処理を、Javaのサンプルコードから読み解いてみます。

判定処理

判定処理は、物体の種類と位置を検知する物体検知タスクのサンプルを見てみました。
プリトレーニングモデルを用いて、短いコードで判定を実現しています。

サンプル(ObjectDetetion.java)

    public static DetectedObjects predict() throws IOException, ModelException, TranslateException {
        Path imageFile = Paths.get("src/test/resources/dog_bike_car.jpg");
        BufferedImage img = BufferedImageUtils.fromFile(imageFile);

        Map<String, String> criteria = new ConcurrentHashMap<>();
        criteria.put("size", "512");
        criteria.put("backbone", "resnet50");
        criteria.put("flavor", "v1");
        criteria.put("dataset", "voc");

        try (ZooModel<BufferedImage, DetectedObjects> model =
                MxModelZoo.SSD.loadModel(criteria, new ProgressBar())) { //(1) 

            try (Predictor<BufferedImage, DetectedObjects> predictor = model.newPredictor()) { //(2)
                DetectedObjects detection = predictor.predict(img); //(3)
                saveBoundingBoxImage(img, detection);
                return detection;
            }
        }
    }
(1)モデルの読込み

モデルの読込みは

MxModelZoo.SSD.loadModel(criteria, new ProgressBar()))

というコードでやっています。

最初の MxModelZoo.SSD というところで、物体検知用のアルゴリズムであるSSDを指定し、
さらにSSDのバックボーンとなる画像判定モデルを何にするかを、
上のcriteriaというMapに詰めて指定して渡しています。
今回は"VOC"(PASCAL VOCのデータセット)で学習されたResNet50 v1のモデルを指定しています。

学習済みモデルは、名前の通りMXNet上の物がサポートされているようです。
AWSさんですし、MXNetになりますよね。

MxModelZooで指定できるモデルは以下のページに表で一覧されています。

djl/mxnet/mxnet-model-zoo at master · awslabs/djl · GitHub

表の各列の意味は次の通りです。

説明
Application 画像分類(Image Classification)やポーズ検知(Pose Estimation)などのタスクの種類。
Model Family SSDなどのモデルの分類名。この名前をMxModelZoo.~のところに指定します。
CriteriaとPossible values サンプルコードであったcriteriaに指定できる条件と値の組み合わせです。

今回やっている物体検知(Object Detection)は現在SSDのみをサポートしており、
バックボーンはVGGやMobileNetが使えるようです。

(2)判定器の生成
Predictor<BufferedImage, DetectedObjects> predictor = model.newPredictor()

で、新しい判定器を生成しています。

(3)画像の判定

メソッドの先頭の

Path imageFile = Paths.get("src/test/resources/dog_bike_car.jpg");
BufferedImage img = BufferedImageUtils.fromFile(imageFile);

で読み込んだ画像を、(2)で生成した判定器に

predictor.predict(img);

で渡し、さらに結果をsaveBoundingBoxImage()というprivateメソッドに渡して、
画像上に検知結果の名称と枠線を描画させています。

得られた画像が次のものです。

f:id:acro-engineer:20200214101006j:plain

リポジトリ内の djl/examples/src/test/resources/ ディレクトリに、
試し斬りに使える画像が置いてあるので、

Path imageFile = Paths.get("src/test/resources/dog_bike_car.jpg");

のところを書き換えて試してみましょう。

学習処理

学習の方は、伝統的なMNISTのサンプルがあったので試してみました。

サンプル(TrainMnist.java)

    public static ExampleTrainingResult runExample(String[] args)
            throws IOException, ParseException {
        Arguments arguments = Arguments.parseArgs(args);

        // Construct neural network
        Block block =
                new Mlp(
                        Mnist.IMAGE_HEIGHT * Mnist.IMAGE_WIDTH,
                        Mnist.NUM_CLASSES,
                        new int[] {128, 64}); //(1)

        try (Model model = Model.newInstance()) {
            model.setBlock(block); //(2)

            // get training and validation dataset
            RandomAccessDataset trainingSet = getDataset(Dataset.Usage.TRAIN, arguments);
            RandomAccessDataset validateSet = getDataset(Dataset.Usage.TEST, arguments); //(3)

            // setup training configuration
            DefaultTrainingConfig config = setupTrainingConfig(arguments); //(4)
            config.addTrainingListeners(
                    TrainingListener.Defaults.logging(
                            TrainMnist.class.getSimpleName(),
                            arguments.getBatchSize(),
                            (int) trainingSet.getNumIterations(),
                            (int) validateSet.getNumIterations(),
                            arguments.getOutputDir()));

            ExampleTrainingResult result;
            try (Trainer trainer = model.newTrainer(config)) { //(5)
                trainer.setMetrics(new Metrics());

                /*
                 * MNIST is 28x28 grayscale image and pre processed into 28 * 28 NDArray.
                 * 1st axis is batch axis, we can use 1 for initialization.
                 */
                Shape inputShape = new Shape(1, Mnist.IMAGE_HEIGHT * Mnist.IMAGE_WIDTH);

                // initialize trainer with proper input shape
                trainer.initialize(inputShape);

                TrainingUtils.fit( //(6)
                        trainer,
                        arguments.getEpoch(),
                        trainingSet,
                        validateSet,
                        arguments.getOutputDir(),
                        "mlp");

                result = new ExampleTrainingResult(trainer);
            }
            model.save(Paths.get(arguments.getOutputDir()), "mlp"); //(7)
            return result;
        }
    }
(1) レイヤ構造を定義

ニューラルネットワークのレイヤ構造を、MLPというクラスに生成させています。
MLPクラスのコードはこちらです。
お手軽に多層パーセプトロンを作ってくれるようです。中は次のようになっていました。

public Mlp(int width, int height) {
    add(Blocks.batchFlattenBlock(width * (long) height))
            .add(new Linear.Builder().setOutChannels(128).build())
            .add(Activation.reluBlock())
            .add(new Linear.Builder().setOutChannels(64).build())
            .add(Activation.reluBlock())
            .add(new Linear.Builder().setOutChannels(10).build());
}

width×heightの入力を受け取り、各層で128個、64個、10個の出力をする層を重ねたネットワークを構築しているようです。
層の内容を変えたければこのクラスでやっているようにadd()メソッドで積み重ねて作れます。

(2) モデルの生成

(1)のレイヤ定義を使って

try (Model model = Model.newInstance()) {
    model.setBlock(block);

で初期状態のモデルを生成しています。

(3) データの準備

getDataset()というprivateメソッドを呼び出して、MNIST用のデータを取得します。
Argument(プログラム引数)を渡しているのは、引数で指定したエポック数とバッチサイズに応じたデータ数を引っ張ってくるためのようです。

RandomAccessDataset trainingSet = getDataset(Dataset.Usage.TRAIN, arguments);
RandomAccessDataset validateSet = getDataset(Dataset.Usage.TEST, arguments);

getDataset()の内部では、 Mnist.builder() という組み込みのMNISTデータロード用クラスにデータを作らせていました。

(4) 学習の設定

setupTrainingConfig()というprivateメソッドの内部で、

return new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
        .addEvaluator(new Accuracy())
        .setBatchSize(arguments.getBatchSize())
        .optDevices(Device.getDevices(arguments.getMaxGpus()));

という処理で学習用の設定インスタンスを生成しています。
プログラム引数のバッチサイズや利用してよいGPU数などをセットし、Softmax関数、クロスエントロピー誤差を指定しています。

(5) 学習器の初期化

(4)で生成した環境設定で、学習器を生成しています。

Trainer trainer = model.newTrainer(config))
(6) 学習開始

学習器やデータを渡して、実際に学習を開始します。

TrainingUtils.fit(
        trainer,
        arguments.getEpoch(),
        trainingSet,
        validateSet,
        arguments.getOutputDir(),
        "mlp");
(7) モデルの保存

model.save()を呼ぶとファイルに学習済みモデルを保存できます。

model.save(Paths.get(arguments.getOutputDir()), "mlp");

感想

基本的な判定と学習を見てみましたが、APIの構成内容がオーソドックスで名前にクセも無いため、
他のディープラーニングフレームワークを知っていれば習得は速そうです。

またGPUは勝手に見つけて勝手に使ってくれるらしく楽です。

OpenCVなどを用いる画像処理ライブラリは、DJL側でラッパーを用意しており、
基本的な操作であれば独自にOpenCVを触らないでよさそうです。

上で紹介した以外のサンプルも同じディレクトリに配置されており、
基本的にはサンプルを真似して使えば一通りのことはできそうだと感じました。
Java上のディープラーニングライブラリとしては、良い候補になりそうです。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの求人 - Wantedlywww.wantedly.com

JJUG CCC 2019 Fallに登壇しました

こんにちは、しんどーです。
最近、トロとパズルにハマっています。この前覚えさせた言葉は「rm -rf /」です。

さて、先日11/23に行われたJJUG CCC 2019 Fallに登壇してきました。

JJUG CCCは日本Javaユーザグループ主催のカンファレンスで、参加者は登録数で1200名を超えたそうです。 自分は参加は9回目で、そのうち登壇は3回目になります。

資料

https://speakerdeck.com/rshindo/jjug-ccc-2019-fall

内容について

今回Java初心者を対象とした「ステップアップセッション」枠でソースコードリーディングについて話しました。

自分も1、2年目のころはOSSのコードを読むのにハードルを感じていて、同じ感覚を持っている人が結構いるんじゃないかなと思ったのがこのネタにした理由です。また、自分の中でコードを読むときに重要視していることを言語化したかったというのもあります。

裏話

ヒイヒイ言いながら資料頑張って作ったところ作り過ぎてしまい、発表では時間が足りなくなり一部カットになってしまいました。聞きに来ていただいた皆さん、すみませんでした。事前練習の繰り返しホント大事ですね!

今回取り上げた2つのライブラリ「Commons DBUtils」と「Javalin」を選んだ理由ですが、

  • コードが大きくない
  • 抽象化が控えめ = 読むのが難しくない
  • DBアクセス、Webといった多くのひとになじみのある領域

といった大体この3点です。

実際、これらのライブラリは機能としてはプリミティブですが、逆を言えばMyBatisやSpring MVCといった高機能なライブラリがコアでやっていることを抑えているとも言えます。大きなライブラリを読むための肩慣らしとしては最適かなーと思っています。ただDBUtilsを知ってる人が皆無だったのは誤算でしたw

f:id:acro-engineer:20191125101347j:plain
満員御礼!

まとめ

次回予告

12/18(水)に行われるSpring Fest 2019で登壇します!

springfest2019.springframework.jp

Spring Boot ActuatorとMicrometerでの運用監視についてお話しします。ぜひ来てね!

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
Java/PythonでモダンなWeb開発をしたいエンジニアWanted! - Wantedlywww.wantedly.com

JJUG CCCにボランティアスタッフとして参加しました!!

皆さんこんにちは。機械学習エンジニアのBuzz@move_fastestです!

先日5/18(土)に開催されたJJUG CCC 2019 Spring にAcroquestの若手3人でボランティアスタッフとして参加してきました!

f:id:acro-engineer:20190522001133p:plain
Acroquestの若手3人

JJUG CCCは日本Javaユーザグループ主催する国内最大のJavaコミュニティイベントです。
今回はなんと一般参加が1000人を超えたようです!!

f:id:acro-engineer:20190522001044p:plain
会場入り口

ボランティアに応募した経緯

社内活動に留まらず、社外活動も活発に行っていきたいと考えていた最中、社内SNSにてJJUGのボランティアスタッフの募集が流れてきました。

Javaは社会人になってから1年間学んできた馴染みのある言語でもあり、自分自身も外部のコミュニティを経験してみたいという思いから応募しました。

ボランティアの活動内容

・当日までの流れ
 基本的にやり取りは全てslackで行いました。
 自身が行うべき役割や当日の動きが事前に決められており、スレッドを確認するのみでした。

・当日の朝
 会場の設営準備を行いました。
 設営準備時間は1時間程度でしたが、事前に準備する項目が決められていたため、効率よく準備ができたと思います。

・セッション中
 セッション会場にて、イスが足りない会場にひたすらイスを分配しました。
 特に今年は来場者が多く、複数のセッション会場で同時にイスが枯渇するという事態が多発したため、大変でした。(でも楽しかったですw)
 
・セッション後
 会場の片付けし、懇親会の準備を行いました。

JJUG基調講演

今回のJJUG総会にて、弊社の谷本が JJUGのリーダーに就任しました。
就任直後でしたがすぐにJJUGの体制見直しを行ったりと、新リーダーとしてやりたいことがたくさんあり、うずうずしているようでした。
そして、総会後には今後JJUGをどんなコミュニティにしていきたいかを熱く語ってくれました!


このようなユーモアな発言をしておりますが、本当に凄い人です・・・・

f:id:acro-engineer:20190522152230p:plain
基調講演

懇親会

JJUG終了後の懇親会では、スポンサーであるLINE様よりお寿司を頂きました。

f:id:acro-engineer:20190522002316p:plain
懇親会のお寿司

お、おいしかった・・・・・

その他にも星野リゾート様によなよなエールの差し入れを頂くなど、このコミュニティの規模の大きさに驚きました。。。。

参加してみた感想

今回このような外部コミュニティにボランティアスタッフとして初めて参加しましたが、非常に楽しかったです。
なぜならば、ボランティアスタッフとして参加することで、「JJUG を自分達で創り上げる」という共通目標を持った仲間と活動をすることができ、当事者意識が生まれ、それらが会終了後の達成感を生んだからです。
このような達成感は講演を聞くだけの参加者には味わうことができない感覚だと思います。
今後もこのような会の運営に携わっていくような、運営側の活動をしていこうと思えた経験でした。

最後に

今回はボランティアスタッフとして運営に注力し、イベントを創り上げることに貢献することができ、Javaコミュニティを盛り上げるという達成感を味わうことができました。
しかしそれと同時に、私も一人のエンジニアとしてこのような大きな舞台で登壇したいという思いが芽生えました。

今後もこのような外部コミュニティで活動することを継続していくのに加え、登壇できるように技術のアウトプットを活発に行っていきます!!

f:id:acro-engineer:20190518202632j:plain
JJUG終了後

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com