AWS Amplify iOSでテキストの解釈を行う #reinvent

ネイティブアプリ用の新しいSDK「Amplify iOS」と「Amplify Android」がプレビューにて公開されました。iOSではPrediction(予測機能)についてCoreMLフレームワークとの組み合わせが実装されています。本記事では、Amplify iOSを使ってテキストの解釈を試してみました。
2019.12.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Amplify iOSではCoreMLフレームワークをサポート!

re:Invent 2019の期間中、ネイティブアプリ用の新しいSDKAmplify iOSAmplify Androidがプレビューにて公開されました。

【速報】モバイル向けの新しいSDK「Amplify iOS & Amplify Android」が公開されました! #reinvent

その中で、iOSでは Prediction(予測機能) について CoreMLフレームワークとの組み合わせ が実装されています。ML/AI系の機能を、AWSサービスとiOS Frameworkを組み合わせ、非常に高い精度の結果セットが得られるようになっています。

CoreMLフレームワークおよびCoreML VisionフレームワークはiOSの機械学習用のフレームワークで、学習モデルなどを扱う際に開発者が専門的な知識を必要とせずに扱えるように補助するフレームワークです。CPU、GPU、ニューラルエンジンを活用し、予測の作成、モデルのトレーニングや微調整をすべてユーザーのデバイス上で行うようになっています。

本記事では、Amplify iOSを使ってテキストの解釈を試してみました。AWSサービスとしてはAmazon Comprehendを利用する形となります。Amplify iOSを通して使うことで、ローカル(オフライン)での実行も可能になります。

インストール

それではまずはインストールしていきます。なお、AmplifyのiOSアプリ向けプロジェクトはすでに作成済みの前提で進めます。また、今回は対話形式でのインストールが必要なためAmplify CLIを利用します。

まずは predictions というプラグインを追加します。

$ amplify add predictions

対話形式で、どのような機能を作りたいか問われます。

? Please select from one of the categories below : Interpret
? What would you like to identify? : Interpret Text
? Provide a friendly name for your resource : interpretText9b0e560f
? What kind of interpretation would you like? : All
? Who should have access? : Auth and Guest users

以下のように分類されているので、自分の作りたい機能に合わせて設定します。本記事では Interpret Text を選択しています。

Interpret Text を選択すると What kind of interpretation would you like? と、どのような種類の解釈を行いたいのか問われます。今回は All としましたが、より限定的な使い方が設定できるようです。

? What kind of interpretation would you like?
  Language
  Entity
  Keyphrase
  Sentiment
  Syntax
❯ All

また、最後の Who should have access?Auth and Guest users を選び、Cognito User Poolsの作成を行うようにします。これはAWSリソースへのアクセスを行うため必須になります。

Successfully added resource identifyTexte454f02b locally

ローカルでの設定が完了したので amplify push でAWSリソースを作成します。

$ amplify push

| Category    | Resource name         | Operation | Provider plugin   |
| ----------- | --------------------- | --------- | ----------------- |
| Predictions | interpretText9b0e560f | Create    | awscloudformation |
| Api         | amplifyDatasource     | No Change | awscloudformation |
| Auth        | amplifysample93d65ae8 | No Change | awscloudformation |

以上でAWSリソースの作成は完了です。

次に AWSPredictionsPlugin というPodを追加します。Podfile 全体としては以下のようになります。

target 'AmplifySample' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for AmplifySample
  pod 'amplify-tools'
  pod 'Amplify'
  pod 'AWSPluginsCore'
  pod 'AWSPredictionsPlugin'
  pod 'AWSMobileClient', '~> 2.12.0'
  pod 'AmplifyPlugins/AWSAPIPlugin'
end

最後に pod install を実行して終わりです。

$ pod install --repo-update

実装

今回はテキストの自然言語処理を行いたいだけなので、iOSアプリでは画面なしで実装します。

まずは AppDelegate でセットアップする処理を書きます。

AppDelegate.swift

import UIKit
import Amplify
import AWSPredictionsPlugin
import AmplifyPlugins

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let apiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels())
        let predictionsPlugin = AWSPredictionsPlugin()
        do {
            try Amplify.add(plugin: apiPlugin)
            try Amplify.add(plugin: predictionsPlugin)
            try Amplify.configure()
            print("Amplify initialized")
        } catch {
            print("Failed to configure Amplify \(error)")
        }
        return true
    }

}

次に ViewController で画像のテキスト解析を行う処理を実装します。interpret() というメソッドを用意しました。文言はAmazon Comprehendの説明文を拝借しています。

ViewController.swift

import UIKit
import Amplify

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        interpret()
    }
    
    func interpret() {
        let text = "Amazon Comprehend is a natural language processing (NLP) service that uses machine learning to find insights and relationships in text. No machine learning experience required."
        let options = PredictionsInterpretRequest.Options(defaultNetworkPolicy: .offline, pluginOptions: nil)
        _ = Amplify.Predictions.interpret(text: text, options: options, listener: { (event) in
            switch event {
                case .completed(let result):
                    print(result)
                case .failed(let error):
                    print(error)
                default:
                break
            }
        })
    }

}

試してみる

それでは実行してみましょう。コンソールログに以下のように出力されるかと思います。

InterpretResult(
  keyPhrases: nil, 
  sentiment: Optional(Amplify.Sentiment(predominantSentiment: Amplify.SentimentType.negative, sentimentScores: nil)), 
  entities: Optional([
    Amplify.EntityDetectionResult(type: Amplify.EntityType.person, targetText: "Amazon Comprehend", score: nil, range: Range(Swift.String.Index(_rawBits: 1)..<Swift.String.Index(_rawBits: 1114112)))
  ]), 
  language: Optional(Amplify.LanguageDetectionResult(languageCode: Amplify.LanguageType.english, score: nil)), 
  syntax: Optional([
    Amplify.SyntaxToken(tokenId: 0, text: "Amazon", range: Range(Swift.String.Index(_rawBits: 1)..<Swift.String.Index(_rawBits: 393216)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 1, text: "Comprehend", range: Range(Swift.String.Index(_rawBits: 458752)..<Swift.String.Index(_rawBits: 1114112)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 2, text: "is", range: Range(Swift.String.Index(_rawBits: 1179648)..<Swift.String.Index(_rawBits: 1310720)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil)), 
    Amplify.SyntaxToken(tokenId: 3, text: "a", range: Range(Swift.String.Index(_rawBits: 1376256)..<Swift.String.Index(_rawBits: 1441792)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.determiner, score: nil)), 
    Amplify.SyntaxToken(tokenId: 4, text: "natural", range: Range(Swift.String.Index(_rawBits: 1507328)..<Swift.String.Index(_rawBits: 1966080)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.adjective, score: nil)), 
    Amplify.SyntaxToken(tokenId: 5, text: "language", range: Range(Swift.String.Index(_rawBits: 2031616)..<Swift.String.Index(_rawBits: 2555904)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 6, text: "processing", range: Range(Swift.String.Index(_rawBits: 2621440)..<Swift.String.Index(_rawBits: 3276800)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 7, text: "NLP", range: Range(Swift.String.Index(_rawBits: 3407872)..<Swift.String.Index(_rawBits: 3604480)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 8, text: "service", range: Range(Swift.String.Index(_rawBits: 3735552)..<Swift.String.Index(_rawBits: 4194304)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 9, text: "that", range: Range(Swift.String.Index(_rawBits: 4259840)..<Swift.String.Index(_rawBits: 4521984)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.pronoun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 10, text: "uses", range: Range(Swift.String.Index(_rawBits: 4587520)..<Swift.String.Index(_rawBits: 4849664)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil)), 
    Amplify.SyntaxToken(tokenId: 11, text: "machine", range: Range(Swift.String.Index(_rawBits: 4915200)..<Swift.String.Index(_rawBits: 5373952)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 12, text: "learning", range: Range(Swift.String.Index(_rawBits: 5439488)..<Swift.String.Index(_rawBits: 5963776)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil)), 
    Amplify.SyntaxToken(tokenId: 13, text: "to", range: Range(Swift.String.Index(_rawBits: 6029312)..<Swift.String.Index(_rawBits: 6160384)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.particle, score: nil)), 
    Amplify.SyntaxToken(tokenId: 14, text: "find", range: Range(Swift.String.Index(_rawBits: 6225920)..<Swift.String.Index(_rawBits: 6488064)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil)), 
    Amplify.SyntaxToken(tokenId: 15, text: "insights", range: Range(Swift.String.Index(_rawBits: 6553600)..<Swift.String.Index(_rawBits: 7077888)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 16, text: "and", range: Range(Swift.String.Index(_rawBits: 7143424)..<Swift.String.Index(_rawBits: 7340032)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.conjunction, score: nil)), 
    Amplify.SyntaxToken(tokenId: 17, text: "relationships", range: Range(Swift.String.Index(_rawBits: 7405568)..<Swift.String.Index(_rawBits: 8257536)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 18, text: "in", range: Range(Swift.String.Index(_rawBits: 8323072)..<Swift.String.Index(_rawBits: 8454144)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.preposition, score: nil)), 
    Amplify.SyntaxToken(tokenId: 19, text: "text", range: Range(Swift.String.Index(_rawBits: 8519680)..<Swift.String.Index(_rawBits: 8781824)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 20, text: "No", range: Range(Swift.String.Index(_rawBits: 8912896)..<Swift.String.Index(_rawBits: 9043968)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.determiner, score: nil)), 
    Amplify.SyntaxToken(tokenId: 21, text: "machine", range: Range(Swift.String.Index(_rawBits: 9109504)..<Swift.String.Index(_rawBits: 9568256)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 22, text: "learning", range: Range(Swift.String.Index(_rawBits: 9633792)..<Swift.String.Index(_rawBits: 10158080)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil)), 
    Amplify.SyntaxToken(tokenId: 23, text: "experience", range: Range(Swift.String.Index(_rawBits: 10223616)..<Swift.String.Index(_rawBits: 10878976)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.noun, score: nil)), 
    Amplify.SyntaxToken(tokenId: 24, text: "required", range: Range(Swift.String.Index(_rawBits: 10944512)..<Swift.String.Index(_rawBits: 11468800)), partOfSpeech: Amplify.PartOfSpeech(tag: Amplify.SpeechType.verb, score: nil))
  ]))

結果は InterpretResult 型で返されます。それぞれ次のような意味があります。

要素 説明
keyPhrases テキスト内の重要なフレーズ
sentiment 感情の解析結果
entities 文章の属するエンティティタイプと信頼度
language テキストの主要言語の解析結果
syntax 一語一語の解析情報

language を見てみると Amplify.LanguageType.english と日本語で解釈されていることが分かります。

明示的にオフラインで使いたい場合は、実行する際のオプション設定を変更します。

let options = PredictionsInterpretRequest.Options(defaultNetworkPolicy: .offline, pluginOptions: nil)
_ = Amplify.Predictions.interpret(text: text, options: options, listener: { (event) in
    switch event {
        case .completed(let result):
            print(result)
        case .failed(let error):
            print(error)
        default:
            break
    }
})

iOSのフレームワークと統合されたAmplify

CoreML Vision FrameworkとAWSのML/AI系サービスの統合によって、かなりパワフルに働くようになりました。

モバイルアプリの場合は常に安定した通信環境が得られるとは限りませんので、本記事でご紹介したような機能を使うアプリの場合は非常に有効な手段だと思います。

Predictionsに含まれる機能は他にもあるので、試していきたいと思います。