まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:watson

このブログエントリは IBM Cloud アドベントカレンダー 2023 に参加しています。12/20 ぶんの記事です(このブログエントリ自体は 12/14 に公開します):
2023112901



IBM Cloud から提供されている機械学習型チャットボット生成の Watson Assistant が、ブランド名称変更により Watsonx Assistant となりました:

https://www.ibm.com/jp-ja/products/watsonx-assistant
2023121401


"Watson Assistant" で検索した結果からブラウズした場合は新ブランドページに推移します:
2023121402



私も「ブランド名の変更があっただけで、中身は変わってないよね?」と思っていました。結論として機能的にはそれほど大きく変わったわけではないのですが、新しくインスタンスを作成した直後の画面が変わっていて、従来のダイアログ機能(インテントやエンティティを定義して会話の流れを作るツールとその機能)を利用する方法が分かりにくくなっていると感じました。これまでの Watson Assistant では普通に使えていたダイアログ機能を新しい UI で使えるようになるまでの手順を以下に紹介します。


まず最初の(名前設定などの)初期設定を一通り実行します。その後に画面左のメニューから下の方にある "Assistant settings" を選択します:
2023121403


画面を下の方にスクロールすると、"Activate dialog" と書かれたボタンがあるのでクリックして、アクティベートします:
2023121404


アクティベート後に再度左メニューを開くと、先ほどまでは表示されていなかった "Dialog" という項目が表示されているはずです。これをクリック:
2023121405


これで従来のダイアログ設計画面が表示されます。ここからインテントやエンティティといった要素を定義できます:
2023121406


一瞬「あれ?ダイアログ無くなった??」とビビりました。 (^^;  よかったよかった。


IBM の新しい AI プラットフォームである IBM watsonx.ai (「ワトソンエックス・エーアイ」)が公開され、IBM Cloud のライトプラン(無料)でもある程度使うことができそうなことがわかったので、まだあまりドキュメントが整備されていなさそうな REST API について調べてみました。そして調べた結果わかった情報を使って、実際に REST API を使って動くサンプルアプリを作り、公開してみました。
watsonx.ai



【事前準備】
このブログエントリに書かれた内容を全て試すためには、IBM Cloud のアカウントが必要です。まだ所有していない場合はこちらから登録してください。なお 2023 年7月時点では新規登録の際にクレジットカード情報の入力が必須です(このブログで紹介する内容は無料範囲でも可能ですが、カードの登録は必須です)。

また今回はプロンプトを使った質問を入力して、その内容に対する回答を取得する、という内容の REST API を実際に動かすのですが、そのためには Watson StudioWatson Machine Learning という2つのサービスを利用する必要があります(いずれも従量課金型のサービスですが、ライトプランを使うことである程度まで無料で使うことができます)。 IBM Cloud のアカウントでログイン後にこれら2つのサービスインスタンスを作っておいてください。


【環境構築】
では実際にプロンプトで指示を行うための環境を構築します。IBM Cloud にログインし、リソースリスト(左メニューの上から2番目)から「AI/機械学習」カテゴリを参照します。他の用途で既に IBM Cloud を使っている場合はここにいくつかのサービスインスタンスが並んでいるかもしれませんが、少なくとも上述の事前準備が完了していれば "Watson Machine Learning" と "Watson Studio" の2つのインスタンスは表示されているはずです。これら2つが表示されていることを確認した上で(実際はどっちでもいいんですが) "Watson Machine Learning" の方をクリックします:
2023071700


クリックした "Watson Machine Learning" インスタンスが表示されます。ここで「IBM Cloud Pak for Data で起動」と書かれた青いボタンをクリックします:
2023071701


初回はこのような画面が表示されます。少し待ちます:
2023071701


少し待つとこのような画面が表示されます。「ML モデルの作成と管理」というダイアログが表示されていますが、実はこの時点では watsonx.ai ではなく IBM Cloud Pak for Data という別プラットフォームの画面になっています。プラットフォームを切り替えるため、この画面では「キャンセル」をクリックします:
2023071702


プラットフォームの切り替えは画面右上のメニューから行うことができます。画面右上から「IBM watsonx.ai」を選択し直します。これでプラットフォームが watsonx.ai 用のものに切り替わります:
2023071703


はじめて watsonx.ai を使う時にはこのような画面が表示されます。利用規約にチェックを入れ、また初回はまだプロジェクトがないので(先に進むためにはプロジェクトが最低1つ必要なので)「サンドボックスプロジェクトの作成」ボタンをクリックします(既に利用済みで、自分のプロジェクトが存在している場合はそのプロジェクトを選択しても構いません):
2023071704


プロジェクトが選択されていると、このような画面(プロジェクトの初期画面)になります。プロンプト指示を試す場合は、一番左の「ファウンデーション・モデルを・・」と書かれたプロンプト・ラボを選択します:
2023071705


プロンプト・ラボを始めて実行するとこのようなダイアログが表示されます。内容を確認して全てチェックします。説明を確認する場合は「ツアーを開始」をクリックしてもよいのですが、飛ばす場合は「ツアーのスキップ」をクリックします。これで環境構築は一通り完了です:
2023071706



【プロンプト実行】
ここまで正しく実行すると下のような画面になり、プロンプト指示を実際に試すことができるようになります。なお特に今回は REST API を操作することを目的としているので、プロンプトを試す前に画面右の「コードの表示」メニューをクリックしておきます:
2023071704


すると画面右側に curl の実行コマンドが表示されます。この後プロンプトで入力をすることになるのですが、その入力処理を curl で実行した場合の実行内容(接続 URL やヘッダ、データの中身)を確認することができるので、この内容を参考に REST API のプログラミングができるようになります:
2023071705


参照ページの例を参考にプロンプトで指示を出してみます。すると実行結果が返ってくるだけでなく、その結果を得るために実行された curl コマンドの REST API パスが /ml/v1-beta/generation/text?version=2023-05-29 であったことや、日本語で指示している場合も特に日本語であることを明示するパラメータが送られていないことを含め、どのような REST API が実行されていたかがわかります:
2023071706


【アクセストークンの取得】
後はこれと同じことを自分のプログラミングの中で実装すればよい、、のですが、この REST API を実行する上で欠くことのできない2つのパラメータがあります:
2023071707


1つは "project_id" というパラメータです。これは名前の通り「プロジェクトのID」で、プロンプト実行前に指定したプロジェクトを一意に示す ID です。この値はプロンプト実行時にブラウザが参照している URL を見ると、URL パラメータの1つとして指定されていることがわかります(つまりブラウザの URL から取得することができます):
2023071708


問題はもう1つの "YOUR_ACCESS_TOKEN" (つまりアクセストークン値)です。これはこの文字列をそのまま使っても正しく実行できません。しかもこの値はウェブブラウザを参照するなどの方法では取得できず、IAM API キーと呼ばれる値とプログラミングによって動的に取得する必要があるものです。この取得方法については本ブログの趣旨とは異なるので詳しくは解説しませんが、詳しくはこちらのドキュメントを参照してください(後述のサンプルでもこの方法でアクセストークンを取得しています)。


【サンプル】
ここまでに記載した情報を使って、実際に動く Node.js のサンプルアプリケーションを作って公開しました:
https://github.com/dotnsf/watsonx


サンプルといっても実体は "POST /api/generate_text" というエンドポイントを1つだけ実装した Swagger ドキュメントベースの API アプリケーションです。起動時に IAM API キーやプロジェクト ID を環境変数に指定することもできますし、API 実行時にパラメータで指定することもできます。

中身を簡単に説明すると、実装はほぼこの app.js ファイル1つだけで、アクセストークンの取得は getAccessToken() 関数で、テキスト生成(プロンプトの実行)は generateText() 関数で実装しています。興味ある方はこれらの関数内の実装部分を参考にしてください(といっても私も上の方法で知った curl コマンドとそのパラメータ指定をそのまま Node.js 内で使ってるだけなんですけど)。

サンプルアプリケーションを実行するには Node.js インストール済みの環境でソースコードを "git clone" して、"npm install" して、.env ファイルに API キーとプロジェクト ID を保存後に "npm start" するとアプリケーションが 8080 番ポートで起動するので、"http://localhost:8080/_doc" にアクセスすると Swagger ドキュメントが開きます:
2023071701


唯一の API である "POST /generate_text" 部分をクリックして開いて "Try it" ボタンをクリックするとパラメータ設定ができる画面になります。API Key や Project ID 、Model ID は環境変数で指定してあればここでは空のままで構いません。必須入力項目といえるのは Input 値くらいで、ここにプロンプトの内容を記載します。最後に "Execute" ボタンで実行します:
2023071702


正しく実行されると、API の実行結果が下部に表示されます。ちなみにこの例では Input が「入力:\nAbout Watson Discovery\\nIBM Watson® Discovery is an intelligent document processing engine that helps you to gain insights from complex business documents.\n翻訳\n」で、その結果が「Watson Discoveryはビジネスドキュメントに関する意見を得るための知能型ドキュメント処理エンジンです。」でした。どこにも「日本語サポート」とも書かれていないし、「日本語で翻訳」とも指定していないのにここまでできるのはそこそこ日本語でもプロンプトに書かれた意図を理解する力があると思っています。なお、このアプリケーションから実行する場合、 REST API 実行時のパラメータで max_new_tokens の値を(デフォルトの 20 から)100 に変更しています。日本語の場合、20 程度だとまともなある程度長い文章を返せなくなってしまうようで、このようにしています:
2023071703


ちなみにこのサービスを無料のライトプランで使う場合、1か月で使えるトークン数は 25000 だそうです。自分がこれまでにどのくらいのトークンを消費しているかは、IBM Cloud のプロジェクト選択画面で「管理」タブから「リソース使用率」を選択した先の画面で確認することができます。ご利用は計画的に:
2023071704


なお REST API のパス(/ml/v1-beta/generation/text)をみても分かると思いますが、現在の API は v1 のベータ版であり、近い将来に仕様含めて変更する可能性が高いと思っています。その辺りもご注意の上で参照してください。


【参照】
IBM watsonx.ai がやってきた
IBM watsonx.ai を試してみた ( コピペ OK )
Documentation ( IBM watsonx.ai 用)



IBM Cloud から提供されている AI サービス IBM Watson の中で「音声→テキスト変換」を行う Speech to Text APIにおいて、2022/05/05 時点ではまだベータ版機能として提供されている "Speaker Labels" 機能を使ってみました。その様子をサンプルソースコードと併せて紹介します。

なお以下で紹介している様子および内容は 2022/05/05 時点のベータ版のものです。今後 API の実行方法や出力フォーマット、価格、提供しているソースコード等も含めて変更になる可能性もあることをご了承ください。


【Speech to Text サービスにおける Speaker Labels 機能とは】
一般的な Speech to Text サービスから提供されている機能の多くは「一人が話している前提」がありました。要は一人の人が話しているという前提で、その音声データをテキスト化する、というものでした。

IBM Watson Speech to Text サービスにおける Speaker Labels 機能はこの点を改良して、「複数人が話している可能性を考慮」した上で音声データをテキスト化するものです。なお、この機能は 2022/05/05 時点においてはベータ版として提供されており、英語に加えてスペイン語、ドイツ語、チェコ語、韓国語、そして日本語に対応しています。詳しくはこちらを参照ください:
https://cloud.ibm.com/docs/speech-to-text?topic=speech-to-text-speaker-labels


【サンプルとその使い方を紹介】
この Speaker Labels 機能を使った Node.js のサンプルアプリケーションを作って公開してみました。興味のある方はこちらから git clone するかダウンロードして使ってください:
https://github.com/dotnsf/s2t_betas


ソースコードを展開後の、アプリケーションの使い方を紹介します。まずアプリケーションを動かすためには Node.js v14 以上及び npm が必要なので、未導入の場合は自分のシステムにあったモジュールをインストールしておいてください:
https://nodejs.org/

また IBM Watson の Speech to Text サービスインスタンスの API Key およびサービス URL も必要です。無料のライトプラン※でも構わないので IBM Cloud 内に作成し、接続情報から API Key およびサービス URL (apikey の値と url の値)を取得しておいてください(すぐ後で使います):
2022050601

※無料のライトプランの場合、変換できるのは1か月間で 500 分ぶんのデータまで、という制約があります。


また実際に Speech to Text で変換する音声データファイルが必要です。特に今回は Speaker Labels 機能を使うため、二人以上で会話している際の音声データが必要です。自分で録音したものを使っても構いませんし、どこかでサンプルデータをダウンロードして用意していただいても構いません。以下の例では、こちらから提供されている日本語会話サンプルデータを使わせていただきました:
https://www.3anet.co.jp/np/resrcs/333020/

上述のページから提供されているサンプルデータをダウンロードし、使えそうな mp3 ファイルをソースコードの public/ フォルダ内にコピーしておいてください。とりあえず 007.mp3 というサンプルはいい感じに2名の男女が会話している様子のデータになっているので、以下はこのファイルをソースコード内の public/ フォルダにコピーできているものとして説明を進めることにします:
2022050602


会話の音声サンプルデータが public/ フォルダ以下に用意できたらアプリケーションを起動するための準備を(1回だけ)行います。まずソースコードフォルダ直下にある settings.js ファイルをテキストエディタで開き、取得した Speech to Text サービスの API Key とサービス URL をそれぞれ exports.s2t_apikey と exports.s2t_url の値として入力した上で保存します:
2022050603


そして依存ライブラリをインストールします。ソースコードフォルダ直下において、以下のコマンドを実行します:
$ npm install

これで起動の準備が整いました。最後にアプリケーションを起動します:
$ node app

成功すると 8080 番ポートでアプリケーションが起動します。実際に利用するにはウェブブラウザで http://localhost:8080/ にアクセスします。すると以下のような画面になります:
2022050601


左上にはソースコードの public/ フォルダにコピーした音声会話データのファイル名が一覧で表示されています。ここから 007.mp3 というファイルを選択してください(これが比較的わかりやすくていい感じの結果でした)。そして POST ボタンをクリックして Speech to Text を実行します:
2022050602


実行と同時に指定した音声ファイルの再生も開始します(つまり音が出ます)。並行して音声の解析が非同期に行われ、解析結果が少しずつ表示されていく様子を確認できます(ここまではベータ版の機能を使っていません):
2022050603


あるタイミングから確定した文節のテキスト内容が複数の色に分類されて表示されます。この色の分類が話している人の分類でもあります(下の結果では茶色の文字との文字になっているので、二人で会話している様子だと判断されていることになります):
2022050604


007.mp3 を最後まで解析し終えると以下のようになりました。(識別精度はともかく(苦笑))2つの文節の中で2人の人が会話している様子だった、と識別された様子がわかります:
2022050605


【サンプルソースコード内を紹介】
最後にこのアプリケーションのサンプルソースコードの内容を紹介しながら、どのように API を実行して、どのような結果を取得しているのか、という内容を紹介します。先に言っておくと、この Speaker Labels 機能を使う上で API の実行方法自体は(オプションを ON にする以外は)以前と全く同じです。実行結果に新しい情報が含まれるようになるので、その部分の対応が必要になります。 また該当部分はすべて app.js ファイル内にあるので、このファイルの内容と合わせて紹介します。

まず 27 行目で定義しているオブジェクトが Speech to Text 実行時のパラメータに相当するものです。この中で日本語変換モデル等を指定していますが、32 行目の speakerLabels: true によって、ベータ版機能である speakerLabels を有効に設定しています:
27: var s2t_params = {
28:   objectMode: true,
29:   contentType: 'audio/mp3',
30:   model: settings.s2t_model,
31:   smartFormatting: true,
32:   speakerLabels: true,
33:   inactivityTimeout: -1,
34:   interimResults: true,
35:   timestamps: true,
36:   maxAlternatives: 3
37: };

実際の音声→テキスト変換は 88 行目の processAudioFile() 関数で行っています。特にこの例では音声データファイルを一括変換する方法ではなく、WebSocket を使った非同期変換(少しずつ変換結果を受け取る方法)である recognizeUsingWebSocket() (90 行目)を使っています。そして SpeakerLabels を有効にしている場合、この実行結果(92行目)は2通り想定する必要があります。1つは「音声→テキスト変換結果」、もう1つは「どの部分を誰が話していたか、の判定結果」です(一括の同期変換を使った場合はこれらをまとめて取得できますが、今回は非同期変換を使っているためこれらの結果がバラバラに返ってくる可能性を考慮する必要があります):
90: var s2t_stream = my_s2t.s2t.recognizeUsingWebSocket( s2t_params );
91: fs.createReadStream( filepath ).pipe( s2t_stream );
92: s2t_stream.on( 'data', function( evt ){
        :



まず「音声→テキスト変換結果」が返ってきた場合です。この場合、92 行目の evt オブジェクト(=テキスト変換結果)は以下のような形で返されます:
        {
          result_index: 0,
          results: [
            { 
              final: true,
              alternatives: [
                {  //. 候補1
                  transcript: "音声メッセージが既存のウェブサイトを超えたコミュニケーションを実現",
                  confidence: 0.95,
                  timestamps: [
                    [ "音声", 0.36, 0.84 ],
                    [ "メッセージ", 0.84, 1.35 ],
                    [ "が", 1.35, 1.59 ],
                       :
                    [ "実現", 4.13, 4.7 ]
                  ]
                },
                {  //. 候補2
                  :
                }
              ]
            }
          ]
        }

まず変換結果をある程度の区切りでひとまとめにしています(ある程度の空白期間が生じるまでを1つの節とみなしています)。その区切りの番号が result_index 値です(上の例では 0 になっています)。そしてテキスト変換した結果が results 内に配列形式で格納されています。各配列要素の中に final というキーがあり、これが true の場合は節として変換結果が確定したことを意味します(false の場合は節が確定する前の、変換途中での結果が返されていることを意味します)。そして altervatives 内にその変換結果が可能性の高い順にやはり配列で格納されています。特にこの部分に注目してください:
                {  //. 候補1
                  transcript: "音声メッセージが既存のウェブサイトを超えたコミュニケーションを実現",
                  confidence: 0.95,
                  timestamps: [
                    [ "音声", 0.36, 0.84 ],
                    [ "メッセージ", 0.84, 1.35 ],
                    [ "が", 1.35, 1.59 ],
                       :
                    [ "実現", 4.13, 4.7 ]
                  ]
                },

文章としては「音声メッセージが既存のウェブサイトを超えたコミュニケーションを実現」というテキストに変換されていることに加え、その自信度が 0.95 であること、そして各単語が現れる音声開始からの通算秒数が timestamps という配列変数内に格納されています。この例だと音声スタートから 0.36 秒後から 0.84 秒後までの間に「音声」と話されていて、次に 0.84 秒後から 1.35 秒後までの間に「メッセージ」と話されていて、・・・といったように変換結果が分類されています(ここ、後で使います)。

次に変換結果として返される可能性のもう1つ、「誰がどの部分を話しているか」の結果が返される場合、evt 変数の内容は以下のようになります:
        {
          speaker_labels: [
            { 
              from: 0.36,
              to: 0.84,
              speaker: 0,
              confidence 0.67,
              final: false
            },
            {
              from: 0.84,
              to: 1.35,
              speaker: 0,
              confidence: 0.67,
              final: false
            },
              :
            {
              from: 4.13,
              to: 4.7,
              speaker: 1,
              confidence: 0.67,
              final: false
            }
          ]
        }

speaker_labels というキーが含まれている場合はこちらのケースと判断できます。そしてその中身は上の例であれば以下のような意味です:

・0.36 - 0.84 秒の間は 0 番目の人(自信度 0.67)
・0.84 - 1.35 秒の間は 0 番目の人(自信度 0.67) (この2つは同じ人)
   :
・4.13 - 4.7 秒の間は 1 番目の人(自信度 0.67)  (上とは別の人)

先程のテキスト変換結果の timestamps 値と合わせて、どの(何秒時点の)テキスト部分を何番目の人が話しているか、がわかるように speaker というラベルが付けられています。後はこれらをうまく組み合わせて、例えばテキストの色を分けて表示するようにしたものが提供しているサンプルアプリケーションです:
2022050600


なお、現時点での仕様としては以下のような制約があるようです:
・「2名で」話している前提で判断するよう最適化されている(実際には3名以上と判断される場合もあるが、あくまで2名の会話であることを想定した上で最適化されてラベルが付けられる)。
・speaker_labels の結果にも最終結果であることを示す final キーは存在しているが、final = true とならずに終わるケースが多い(なので、現状ここは無視してもよさそう)。


この辺りはあくまでベータ版での仕様なので、精度含めて今後の変更の可能性もあると思っています。ただ少なくともベータ版の現時点ではこの speaker_labels は無料で(無料のライトプランでも)使える機能のようで、今のうちから色々試してみたいと思いました。複数人の会話音声データから複数人の会話テキストを取り出せるようになると会議の議事録とかにも使えそうで、使い道の幅が大きく広がると期待しています。


IBM Watson から提供されている AI 系 API の中で、自然言語テキストを分類する機能を持った NLC(Natural Language Classifier) の提供が終了することがアナウンスされました。2021年9月9日までは新規インスタンスの作成が可能ですが、それ以降の新規作成はできません。また既存インスタンスは2022年8月8日まで利用できますが、それまでに移行先を決め、その移行作業を済ませておく必要があります:
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating


NLC は個人的にも IBM Watson を使い始めるきっかけになった API で、思い入れの深いサービスだったりします。NLC を使って作った多くのデモアプリは WordCamp Tokyo や特定のお客様向けに作ったものを含めて多くの場で紹介させていただきました。感慨深いものがあります。


上記アナウンス内で、公式な移行先として同じ IBM Watson の NLU(Natural Language Understandings) が紹介されています。が、具体的な移行手順や方法に関する情報が少ないこともあり、実際に自分が NLC API を使って開発したアプリケーションをどのように NLU に移行すればよいかがわかりにくいように感じました。 自分自身の場合は普段 Node.js を使ってアプリケーションを開発しているのですが、Node.js の場合は具体的にどのようにすれば NLC から NLU へ移行できるか? という調査をしてみました。実際に Node.js でプログラムを書き、具体的にはまず NLC API を使って、
 ・認証
 ・日本語データ学習
 ・学習状況確認
 ・問い合わせ(分類)
 ・学習の削除
といった5種類のオペレーションを行えるようなアプリケーションを作りました。 そしてそのアプリケーションを NLC から NLU へ実際に移植する、といった作業を行ってみました。結論として上記5種類のオペレーションは NLU でも NLC 同様に行うことができました。ただ API には互換性はなく、ソースコードレベルではそれなりに変更が必要になるため、どの程度の変更が必要になるのか、という調査の意味も含めて作業した様子を以下に紹介します。

なお、NLC は IBM Cloud の(無料版の)ライトアカウント向けには提供されていない API である点に注意してください。ライトアカウントの状態で NLC の新規インスタンスを作成することは(9月9日以前であっても)できません。ベーシック以上の(有償の)アカウントであれば無料枠含めて提供されている API です。 一方の NLU はライトアカウントでも利用することが可能ですが、ライトアカウントの場合はライトプラン(無料)の NLU を1インスタンスのみ作成でき、1インスタンスにつき1モデルだけ学習で作成できます(要は複数の学習モデルを作成するにはベーシック以上のアカウントで、有料プランを選択する必要があります)。
2021082200



【サンプルコードのダウンロード】
以下で紹介する一連の手順を行うための Node.js 用サンプルコードを作って公開しました:
https://github.com/dotnsf/nlc2nlu

git clone するかコードをダウンロード&展開してください。以下の3つのフォルダが含まれています:
|- csvtool
|- nlc
|- nlu

csvtool は実際に NLC や NLU で使うサンプルの学習データを作成するツールです。NLC や NLU を既に使っていて、どのような学習データを用意すればよいかわかっている場合は、ご自身で学習データ(CSV ファイル)を用意いただいてもかまいませんが、そうでない人向けに Yahoo! ニュースの RSS(https://news.yahoo.co.jp/rss) を使って、その場で学習データを作成できるようにしたものです。

nlc と nlu は名前の通りで、学習データを学習させて、学習状況を確認しながら学習が完了したら、実際に適当な日本語テキストを送信して、学習データに基づくテキスト分類を実施します。また実施後に学習データを削除する、といった操作も可能です。これらの内容を NLC および NLU それぞれで行えるようにしたツールが含まれています。

ではそれぞれのフォルダの使い方を説明します。


【csvtool : 学習データの用意】
csvtool/csvgenerator.jsYahoo! ニュース RSS から学習データとしての CSV ファイルを作成します。NLC / NLU とも学習データは以下のフォーマットの CSV ファイルを用意します※:
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
  :

※ NLU は JSON フォーマットでも学習データを準備して読み込むことが可能ですが、NLC からの移植を考えると NLC と同じ条件で学習データを用意するべきと考え、同じ CSV ファイルを学習データとして使うことにします。

このような(NLC 向けの)学習データを既にお持ちであればそれを使って後述の作業を続けていただいてもかまいませんが、多くの人はそのような学習データを持っていないと思うので、新たに用意することにします。その場合は csvtool/csvgenerator.js を使います。

実行方法は単純で、csvtool フォルダに移動し、まず普通に $ npm install を実行して依存ライブラリを導入します:
$ cd csvtool

$ npm install

その後、node コマンドでこのファイルを実行します。実行コマンドの最後に出力先 CSV ファイル名を指定します(以下の例だと同一フォルダ内の csvfile.csv):
$ node csvgenerator csvfile.csv

実行したタイミングで Yahoo! ニュースの RSS を参照して、その時点のニュースを取得して CSV ファイルに書き出します。なお、このツールでは「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」の5つのカテゴリーのニュースを収集します。 ツールの実行が完了すると、指定した CSV ファイルが以下のような内容で生成されます:
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
   :

ここまでできれば学習データの準備は完了です。ではこのデータを NLC と NLU それぞれで学習させて問い合わせる、という作業をこれ以降で行っていきましょう。


【nlc : NLC で操作】
まずは NLC API を使って開発したアプリでこのデータを学習させ、NLC API を使って問い合わせを行ってみましょう。先ほどダウンロードしたソースコードの nlc フォルダ内にある nlc/nlc.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlc.js はコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlc create [csvfilename]  ・・・データ学習

$ node nlc status  ・・・学習状況確認

$ node nlc classify [日本語テキスト] [classifier_id]  ・・・問い合わせ(分類)

$ node nlc delete [classifier_id]  ・・・学習データ削除

では実際に nlc ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLC インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082201


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlc/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlc_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlc_url = 'サービス資格情報内の url 属性値';
exports.nlc_name = 'nlc2nlu';
exports.nlc_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlc create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlc create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlc status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています(データ量にもよりますが、自分が試した時はこうなるまでおよそ 10 分程度かかりました)。同時に表示されている classifier_id の値はこの後で使うので合わせてメモしておきましょう:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "application/json",
    "x-xss-protection": "1",
    "content-security-policy": "default-src 'none'",
    "x-content-type-options": "nosniff",
    "cache-control": "no-cache, no-store",
    "pragma": "no-cache",
    "expires": "0",
    "content-length": "428",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-request-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-global-transaction-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "22",
    "date": "Tue, 24 Aug 2021 05:48:32 GMT",
    "connection": "close"
  },
  "result": {
    "classifier_id": "e87efex297-nlc-650",
    "name": "nlc2nlu",
    "language": "ja",
    "created": "2021-08-24T05:45:20.090Z",
    "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650",
    "status_description": "The classifier instance is now available and is ready to take classifier requests.",
    "status": "Available"
  }
}


学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。日本語テキストと上述で確認した classifier_id 値を指定して、以下のようなコマンドを実行します:
$ node nlc classify [日本語テキスト] [classifier_id]

すると以下のような結果が表示されます。指定した日本語テキスト(「パッキャオがんばれ」)を学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。この例では「スポーツ」の可能性が 50% 強である、と判断されているようです:
$ node nlc classify パッキャオがんばれ e87efex297-nlc-650
{ "status": 200, "statusText": "OK", "headers": { "content-type": "application/json", "x-xss-protection": "1", "content-security-policy": "default-src 'none'", "x-content-type-options": "nosniff", "cache-control": "no-cache, no-store", "pragma": "no-cache", "expires": "0", "content-length": "679", "strict-transport-security": "max-age=31536000; includeSubDomains;", "x-dp-watson-tran-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-request-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-global-transaction-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "server": "watson-gateway", "x-edgeconnect-midmile-rtt": "7", "x-edgeconnect-origin-mex-latency": "43", "date": "Tue, 24 Aug 2021 05:51:22 GMT", "connection": "close" }, "result": { "classifier_id": "e87efex297-nlc-650", "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650", "text": "パッキャオがんばれ", "top_class": "スポーツ", "classes": [ { "class_name": "スポーツ", "confidence": 0.5022989563107099 }, { "class_name": "エンタメ", "confidence": 0.20692538301831817 }, { "class_name": "経済", "confidence": 0.1200947580342344 }, { "class_name": "科学", "confidence": 0.11456564859743573 }, { "class_name": "IT", "confidence": 0.05611525403930185 } ] } }


作成した学習データを IBM Watson NLC から削除するには以下のコマンドを実行します:
$ node nlc delete [classifier_id]

ここまでのオペレーションで「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを NLC API を使って実現し、そのアプリケーションを実行する方法を紹介しました。


【nlu : NLU で操作】
では改めてこの nlc アプリケーションを NLU API で作り直し、同じ4つのオペレーションができることを確認してみます。先ほどダウンロードしたソースコードの nlu フォルダ内にある nlu/nlu.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlu.js もコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlu create [csvfilename]  ・・・データ学習

$ node nlu status  ・・・学習状況確認

$ node nlu analyze [日本語テキスト] [model_id]  ・・・問い合わせ(分類)

$ node nlu delete [model_id]  ・・・学習データ削除

※前述の NLC では問い合わせ時に "classify(分類)" という命令を指定していましたが、NLU では同じ作業を "analyze(解析)" と表現しているようです。またパラメータとして指定する ID も NLC では "classifier_id(分類ID)" だったのですが、NLU では "model_id(モデルID)" という表現になっていました。

では実際に nlu ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLU インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082401


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlu/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlu_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlu_url = 'サービス資格情報内の url 属性値';
exports.nlu_name = 'nlc2nlu';
exports.nlu_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlu create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlu create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlu status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています。同時に表示されている model_id の値はこの後で使うので合わせてメモしておきましょう。この辺りまでは前述の NLC の時とほぼ同様ですね:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "x-powered-by": "Express",
    "content-type": "application/json; charset=utf-8",
    "content-length": "401",
    "etag": "W/\"191-CBnewThU0u/CjNzD01hil1wp9qo\"",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-request-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-global-transaction-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "228",
    "date": "Tue, 24 Aug 2021 05:54:08 GMT",
    "connection": "close"
  },
  "result": {
    "models": [
      {
        "name": "nlc2nlu",
        "user_metadata": null,
        "language": "ja",
        "description": null,
        "model_version": "1.0.0",
        "version": "1.0.0",
        "workspace_id": null,
        "version_description": null,
        "status": "available",
        "notices": [],
        "model_id": "619ad785-0b0c-4981-8217-bd51064896a3",
        "features": [
          "classifications"
        ],
        "created": "2021-08-24T05:43:32Z",
        "last_trained": "2021-08-24T05:43:32Z",
        "last_deployed": "2021-08-24T05:50:10Z"
      }
    ]
  }
}

学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。NLC では "classify" と指定していた部分は NLU では "analyze" となる点に注意してください。日本語テキストと上述で確認した model_id 値を指定して、以下のようなコマンドを実行します:
$ node nlu analyze [日本語テキスト] [model_id]

すると以下のような結果が表示されます。指定した日本語テキストを学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。同じ学習データで同じ問い合わせをして、こちらでも「スポーツ」の可能性が高いと判定されていますが、その確率や2位以下の結果には違いがあることがわかります:
$ node nlu analyze パッキャオがんばれ 619ad785-0b0c-4981-8217-bd51064896a3

{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "server": "watson-gateway",
    "content-length": "499",
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-cache, no-store",
    "x-dp-watson-tran-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15, 93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "content-security-policy": "default-src 'none'",
    "pragma": "no-cache",
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY",
    "x-xss-protection": "1; mode=block",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-request-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-global-transaction-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-edgeconnect-midmile-rtt": "10",
    "x-edgeconnect-origin-mex-latency": "354",
    "date": "Tue, 24 Aug 2021 05:56:16 GMT",
    "connection": "close"
  },
  "result": {
    "usage": {
      "text_units": 1,
      "text_characters": 9,
      "features": 1
    },
    "language": "ja",
    "classifications": [
      {
        "confidence": 0.402721,
        "class_name": "スポーツ"
      },
      {
        "confidence": 0.326038,
        "class_name": "経済"
      },
      {
        "confidence": 0.314985,
        "class_name": "IT"
      },
      {
        "confidence": 0.255796,
        "class_name": "エンタメ"
      },
      {
        "confidence": 0.21978,
        "class_name": "科学"
      }
    ]
  }
}


作成した学習データを IBM Watson NLU から削除するには以下のコマンドを実行します:
$ node nlu delete [model_id]

ここまでのオペレーションで NLC 同様に NLU でも「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを API で実現し、そのアプリケーションを実行する方法を紹介しました。とりあえず、この4つのオペレーションについては NLC から NLU へ移行することはできそうだ、という感触が持てる結果になりました。


【API レベルの移行作業】
同じオペレーションを行う NLC のツールと NLU のツールを実際に Node.js + IBM Watson SDK で開発してわかったことを記載しておきます。

まず API に互換性はありません。IAM を使った認証部分についてはほぼ同じなのですが、今回行った4つのオペレーションを実現するためのそれぞれの API は NLC のものと NLU のものは全く異なります:
NLC の関数 オペレーションの種類 NLU の関数
IamAuthenticator() 認証 IamAuthenticator()
createClassifier() 分類器/モデルの作成 createClassificationModel()
listClassifiers() 作成した分類器/モデルの参照 listClassificationModels()
classify() 分類/解析 analyze()
deleteClassifier() 分類器/モデルの削除 deleteClassificationModel()


パラメータの指定方法など、詳しくはソースコード(nlc/nlc.js / nlu/nlu.js)を参照していただきたいのですが、大まかには上記表のような関数の違いがあります。特に Node.js + IBM Watson SDK を使っているアプリケーションにおいて NLC から NLU へ移植する場合は、表の左列にある関数を使っている箇所を右列の関数に置き換える、というのが大まかな流れになると思います(実際にはパラメータの指定方法だったり、返り値のフォーマットの違いもあったりするので、単なる文字列置換というわけにはいかないと思います)。また NLC では「分類器(Classifier)」と呼んでいたものが NLU だと「分類モデル(ClassficationModel)」と呼び名が変わっていたりもするので、資料を参照する場合の注意も必要だと思います。

加えて、上記のように NLC を使って問い合わせた結果と NLU に移植後に問い合わせた結果は必ずしも同じ結果とはいきません。このあたりの整合性についても移植の前後で意識しておく必要があります。

とはいえ、少なくとも API のレベルで NLC から NLU へアプリケーションを移植することは不可能ではなさそう、という感触も得ることができました。主要なオペレーションに関しては上述の表を使って関数を新しいものに置き換え、実行結果のフォーマットの違いをプログラミング内で吸収することができれば、ある程度の実現目途は立ちそうだと思っています。


今回の NLC のサービス終了自体は残念ではありますが、一方で見方を変えると、これまで有償サービスでしか提供されていなかった NLC の機能が、無料のライトプランで使える NLU でも利用できるようになった、とも言えます。API レベルでの互換性はありませんが、機能的には珍しくこれまでの機能の多くが移植されていて、「アプリケーションの作り変え」による対応が可能なレベルでマイグレーションができるようになっていると感じました。「料金的にも発展的なサービス統合」であると感じています。


Node.js 以外の開発言語での場合や、IBM Watson SDK の利用有無の違いをどこまで吸収できるかまでを調査したわけではないのですが、一部の言語については後述の参考資料の中でサンプルコードもあるようなので、別環境においてもぜひ挑戦していただき、情報が共有されていくことを願っています。



【参考資料】
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating

https://cloud.ibm.com/apidocs/natural-language-understanding?code=node


(注 このブログを書いた時点では 2021/02/12 だった更新期限は 2021/05/25 に変更されました)


IBM Watson サービスのエンドポイント URL が更新され、2021年2月12日5月26日に旧URLが廃止される予定です:
IBM Watsonサービスのネットワーク分離機能拡張のためのIAMの更新


IBM Watson の各種サービス API を(以前から)使っていて、そのエンドポイント URL のホスト部分が *.watsonplatform.net というパターンになっている場合にアプリケーションが正しく動作しなくなるなどの影響を受けます。その場合は月の旧URL廃止前に後述の作業を行って、新しい URL に更新する必要があります。

以下、Watson NLC(Natural Language Classifier) を例に対応手順を含めて個人でまとめたので紹介します。NLC 以外のサービスでも概ね同様ですので参考にしてください。また詳しくは後述のリンク先も参照ください。


【現在使っている Watson API のエンドポイント URL を確認】
まず今回の作業は例えば Watson Assistant の画面を使って作業しているだけなど、外部アプリケーションから API を使って呼び出したりしていない場合は関係ありません。apiKey を指定してプログラミングで Watson API を外部から呼び出す形で利用しているケースが対象となります。

現在 Watson API を使ってアプリケーションを動かしている場合、まずはその API のエンドポイントが旧 URL を使っているのか新 URL を使っているのかを確認します(新 URL であれば後述の作業は不要です)。

例えば Watson NLC を使ったアプリケーションであれば、IBM Cloud にログインし、リソース画面のサービス一覧から利用している該当サービス(Watson NLC サービス)を選択します:
2020103001


選択したサービスの概要が表示される画面内に API Key と URL が表示されています(※)。この URL という部分に着目してください:
2020103002


上図の例では
  https://gateway-tok.watsonplatform.net/natural-language-classifier/api
と表示されている部分です。ここがこのように watsonplatform.net という文字を含んでいる場合は旧 URL を利用しています。一方、ここが api.*****.*****.watson.cloud.ibm.com というパターンになっている場合は新 URL を使っています。

※API Key と URL は「サービス資格情報」メニューからも確認できます。

ここで新 URL を使っていることが確認できた場合は後述の作業は不要です。旧 URL を使っている場合は続けて対処が必要です。


【変更先の Watson API 新エンドポイント URL を確認】
上述の作業で旧 URL を使っているアプリケーションは利用エンドポイント URL を新 URL に変更する必要があります。

まず新しい URL を確認するために新しいサービス資格情報を作成する必要があります。上述の作業に続けて画面左のメニューから「サービス資格情報(Service credentials)」を選択し、新しく資格情報を追加して作成します(その際に現在使っている資格情報と同じロールを指定して作成します):
2020103003


追加された資格情報の名前の左側にある小さな矢印をクリックして展開します:
2020103004


Watson サービスの種類によっても異なりますが、概ね以下のような JSON フォーマットの情報が含まれた内容になっています(一部 ***** でマスクしています):
{
  "apikey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "iam_apikey_description": "Auto-generated for key *********************",
  "iam_apikey_name": "Service credentials-1",
  "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
  "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::********************::serviceid:ServiceId-*************************",
  "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/*************"
}

この JSON の中の url の値(上図では https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/************* )が新 URL です(実際の文字列パターンは使っている IBM Watson サービスの種類やロケーションによって異なります。また最後の ***** でマスクされている部分はインスタンス ID という個別の文字列となります)。アプリケーション内の旧 URL が使われている部分をこの新 URL に変更する必要があります。 また同時に旧アプリケーションで使われている apiKey も新しく作成したもの(上図の apikey で表示されている値 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX )に書き換える必要があります。


【変更作業】 
ここまでの作業で変更が必要な箇所と、変更後の値がわかりました。アプリケーションのソースコードを編集し、旧 URL (上述例では https://gateway-tok.watsonplatform.net/natural-language-classifier/api)が使われている部分を新 URL (上述例では https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/*************)に、また古い apiKey が使われている部分を新しい apiKey の値(上述例では XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)に、それぞれ書き直して保存し、新しいコードで動作確認をしてください。

なお、NLC のように学習が必要な API も再学習の必要はありません。学習済みのデータへそのまま問い合わせが利用できるはずです。


以上、個人でまとめたものですが、背景や詳細な情報はソリューションブログや IBM Cloud Document に記載されています。以下情報も参考にしてください:
IBM Watsonサービスのネットワーク分離機能拡張のためのIAMの更新
Updating endpoint URLs from watsonplatform.net(英語)

このページのトップヘ