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

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

タグ:api

Watson API の1つで、複数の選択肢の中から自分の与えた条件に合うアイテムをトレードオフ判断で絞り込んでくれる、という便利な Tradeoff Analytics API があります(ありました)が、残念ながら5月11日をもって新規インスタンスの生成ができなくなってしまいました(作成済みインスタンスは2018年4月まで使えるようです。詳しくはこちら)。

Bluemix に Watson API が追加された当初から存在していた API の1つであるだけでなく、個人的にもデモなどで使っていて、非常にわかりやすくて便利な API だっただけに残念でした。

このアナウンスがされたタイミングから準備はしていたのですが、実はこの Tradeoff Analytics と同様の(近い互換性を持つ)API を自分でオープンソース化前提で作っています。まだ途中といえば途中ですが、制約付きで一応動く状態にはなっています。新インスタンスが生成できなくなったこのタイミングで一旦公開してみようと思いました:
https://github.com/dotnsf/yatradeoff


なお、この API を実際にアプリケーションサーバー上で稼働させているものもこちらに用意しました。ただ API だけ使いたい人はこちらをどうぞ:
https://yatradeoff.au-syd.mybluemix.net/dilemmas


この URL にブラウザで GET アクセスしても何も起こりません(エラーメッセージが表示されるだけ)が、POST アクセスすると API として動きます。その際にはユーザー名: username &パスワード: password を指定してください(オリジナル同様 Basic 認証です)。


元の Tradeoff Analytics API 同様、/dilemma という POST API エンドポイント1つだけが定義されています。ポストする JSON 型インプットデータのフォーマットには互換性があります。

API の実行結果としての JSON データにも互換性をもたせました。ただ現状はオリジナルのサブセットになっています。現状、以下の3点の制約事項があります:
(1) preferable_solutions データは常にブランクになります
(2) 解析結果は solutions に含まれますが、'FRONT' ステータスを持つデータだけが含まれます
(3) map information を出力するようパラメータで指定しても map information は返されません



ただ1点、オリジナルにはなかった優先順位を考慮する機能も加えています。エンドポイント URL のパラメータに prioritised=1 を追加して実行(例: /dilemma?prioritised=1)すると、インプットデータの JSON データは優先順位順に解釈されます(先にある方が高い優先順位であると解釈されて実行されます。例えば「項目AとBを両方ともトレードオフの材料にする」のではなく、「項目AとBを両方ともトレードオフの材料にするが、Aの方をBよりも優先する」というトレードオフを可能にしています)。


そして、このパラメータを付けて実行した時の API 実行結果も優先順位順に返されます。先にある方がよりオススメな結果、ということになります。まあ preferable_solutions の代わりという位置付けです。


※実はこの機能を付けたくて自分で API を改良した、という経緯があります。


結果の JSON テキストの solutions 内がトレードオフの結果です。オリジナル API では全ての選択肢に対して 'FRONT' か 'EXCLUDE' かの結果を含めていましたが、現状この API では 'FRONT' のもの(トレードオフ判断の結果、選択肢の候補として残ったもの)だけが返される点に注意してください。


IBM Watson Summit 2017 開催記念作品!


自分だけではないと思いますが、可愛らしいフリー素材を数多く公開していただいている「いらすとや」http://www.irasutoya.com/)さんには、大変お世話になっております。



僕の場合はプレゼンテーション内のイラストに多く使わせていただいています。中にはいらすとやさんの素材だけでサイトや資料を作ってしまう職人さんもいらっしゃるようです。

ある程度「いらすとや」を使っていて感じたことは「目的の素材をうまく見つけるのが難しい」ということです。「昔こんな感じのイラストを見た記憶があるんだけど、どのカテゴリーだったっけな?」とか、「この人が使ってるこのイラストと同じものを使いたい」とか、自分の記憶が曖昧だったり、これというキーワードが思いつかない時に目的のイラストをうまく検索できないことがたまにあるのでした。


で、その解決策になるかどうかわかりませんが、興味半分でこんなサイトを作ってみました:
「いらすとや検索」



↑見た通りのサイトです。「なんとなく」覚えているイラストをなんとなく描いて、search して、そのイラストに似た「いらすとや」画像を探す、というものです。PCであればマウスで、スマホの場合はタップで描きます。検索結果は最大100件表示され、その中に含まれていれば目的のページに(クリックで)移動できる、というものです:
2017042501

    ↓

2017042502


例えばこの↑例、「鳩」で検索すればすぐに見つかりますが、「鳥」で検索するとなかなか候補が出てきません。「鳩のイラスト」とまで認識できていればテキスト検索でも探せるのですが、そこまでハッキリを覚えていないようなケースでも「たしかこんな感じの・・・」というイラストが描ければ検索できるようになっています(たぶんw)。


今のところ描くイラストは黒線一本のみで描く必要があります。描き直しに消しゴムなどはなく、reset する必要があります。編集機能にはまだ制約が多いですが、シンプルさを重要視しました(ということにします)!
2017042503



なお、このサイトはコグニティブエンジンである IBM WatsonVisual Recognition(画像認識) API を使って、あらかじめ学習させたイラストからの類似画像を人工知能のテクノロジーを使って検索する、という仕組みで実装しています。いらすとや内の全ページをクロールする方法が分からなかったので、現在は「リクエスト」ラベルから辿れる画像を対象にしています。仕組みはシンプルですが、実はそこそこなテクノロジーが裏に潜んでいたりします。


・・・まあ、ネタにどうぞ(笑)。
 

また、このアプリを作る上で、以下の2つの情報を参考にしました。HTML5 の Canvas にマウス移動(とスマホのタッチ)で線を描画するワザと、特にスマホのタッチで描画をする際に画面のスクロールを強制的に止めるワザです:

JavaScript でマウス座標を取得し、Canvas上に線を描画


先日、IBM の NoSQL DBaaS である Cloudant のデータベース単位でのバックアップ/リストアを行うツールを作って公開しました:

本エントリでは、そもそも何故このようなツールを作ったのか、実現においてどのような制約事項があり、どのように制約を回避したか、その背景を技術者視点で紹介します。


【そもそもそんなツールが必要なのか?】
「必要なのか?」と言われると、結構鋭い視点だと思います。Cloudant はいわゆる「マルチマスターレプリケーション」型の DBaaS であり、データは複数のサーバーインスタンスに分散して格納されます。つまり「どこか1つのサーバーが死んだ場合でもデータがどこかに残っている」ことはこの仕組自体の中で提供されています。これで充分、と考える人や用途であれば不要という判断もアリです。

とはいえ、データベースを手軽に丸ごと引越したり、(精神衛生上の問題から)手元にバックアップを残しておきたいと考えたり、そのバックアップファイルをストレージに保存してストレージ代を稼ぎたいクラウドベンダー/インテグレーター側の事情(苦笑)などからバックアップとリストアを行う需要はあると思っています。


【ツールがないとバックアップ/リストアはできないのか?】
これも難しい質問です。まず Cloudant はいわゆる CRUD 操作に REST API が用意されており、基本的にはこの REST API を使って読み書き更新削除検索・・といった操作を行うことになります。この API のリファレンスはこちらを参照ください:
https://console.ng.bluemix.net/docs/services/Cloudant/api/index.html#api-reference

このリファレンスを読むと分かるのですが、データベースに対して「データや添付ファイルも含めた全ドキュメントデータを取得する API」も「複数のドキュメントデータをまとめて書き込む API」も、どちらも存在しています。以下の記述では前者を「データ取得 API」、後者を「データ書込 API」と呼ぶことにします。

これら2つの API を組み合わせればバックアップとリストアは実現できそうに見えます。が、実はいくつかの問題があり(一部は後述します)、その中でも最も大きなものは「データ取得 API で得られるアウトプットデータと、データ書き込み API でインプットするデータのフォーマットが異なる」という点です。より正確に表現すると「データ取得 API で得られるアウトプットデータを、そのままデータ書込み API のインプットデータとして使った場合、書込み処理自体は成功し、全てのデータがインポートされるが、書き込まれた個々のドキュメントデータの JSON データとしてのフォーマット(データ階層)が元のデータのフォーマットとは異なる」という結果が待っているのでした。つまりバックアップ元とリストア先のデータベースの中身(データの JSON のフォーマット)が同じではなくなってしまうのでした。


シンプルに表現すると、バックアップ元のデータベースに以下のようなデータが入っていたとすると、、
{ "_id": "001", "name1": 123, "name2": "value1", "name3": true },
{ "_id": "002", "name1": 456, "name2": "value2", "name3": false },
{ "_id": "003", "name1": 789, "name2": "value3", "name3": true },
  :

上記 API でバックアップ&リストアした先のデータは以下のようになります:
{ "_id": "001", "doc": { "_id": "001", "name1": 123, "name2": "value1", "name3": true } },
{ "_id": "002", "doc": { "_id": "002", "name1": 456, "name2": "value2", "name3": false } },
{ "_id": "003", "doc": { "_id": "003", "name1": 789, "name2": "value3", "name3": true } },
  :

バックアップ元では、例えば "name1" という属性の値を参照しようとすると、(document).name1 でアクセスできた内容が、リストア先だと (document).doc.name1 という方法でアクセスすることになる、という違いが生じます。


ここは考えようで、「元のデータが残っているかどうか」という観点では残っているといえます。ただ JSON データとして見たときの階層情報が異なっていて、全く同じデータではない(特定の属性の値を取得する場合の取得方法は異なる)のです。これを許容できるかどうかの問題とも言えますが、一般的にバックアップ/リストアという言葉からイメージするのはデータフォーマットの変更が許されるものではないと思っています。その意味では上記 API をそのまま使って実現する方法は選択できなくなります。

また細かいことですが、データ書込み API を実行するには事前にデータベースを新規に作成しておく必要があります。わざわざ「データベース作成+文書リストア」という2度手間をかけるのも不便だし、そういった利便性まで含めて考えると1発で実現できるなんらかのツールがあるべきだし必要、という判断になります。


【ツールでの実現方法】
上記の理由により、リストア先でもデータフォーマットを変えずに復元できるよう、API をそのまま使う方法ではなく、処理の途中にフォーマット変換を行うようなバックアップ/リストア処理を行うツールを作ります。

この時点で考えられる実装アルゴリズムには以下のようなものが挙げられます:
(1) バックアップ時には上記のデータ取得 API をそのまま使い、その結果をバックアップファイルとする。リストア時にはバックアップファイルからデータを1つずつ取り出し、リストアしたい部分だけを取り出して1件ずつ書き込む。
(2) バックアップ時には上記のデータ取得 API をそのまま使い、その結果をバックアップファイルとする。リストア時には、まず上記のデータ書込み API で一括書込みした時にデータフォーマットが乱れないような形にバックアップファイルを加工する。その上でデータ書込み API を実行して一括書き込みする
(3) バックアップ時にはまず上記のデータ取得 API をそのまま使い、その結果をデータ書込み API 用に加工した上で保存し、バックアップファイルとする。リストア時にはデータ書き込み API をそのまま実行して一括書き込みする
  :


いずれも理論的には実現できそうなのですが、ここで API とは別の制約が問題になります。上記の (1) の方法で(データを1件ずつ)リストアすることは可能なのですが、その際に「Cloudant のスタンダードプランでは 1 秒間に 10 件しか書込みリクエストを処理できない」という制約事項を意識する必要があります。例えばバックアップファイルに 11 件以上のドキュメントが存在していた場合、データを書き込む API は1秒間に 10 回しかコールできません(どこまで厳密にこの制約が管理されているのかはわかりませんが、私自身がリクエスト上限によるエラー "You've exceeded your current limit of 10 requests per second for write class. Please try later." の発生を確認しています)。このエラーを回避しようとすると、プログラム内で実行タイミングを測り、10回書込みを実行したら、次の書込みリクエストは1秒以上待ってから実行する、という本質的ではない管理が必要になります(そしてこれは Node.js のような非同期処理を基本とする実行環境では非常に面倒な問題になります)。また仮にそこまで作り込んだとしても、多くのドキュメントを持つデータベースのリストアには非常に多くの時間がかかり、その多くは待ち時間となる可能性が高くなります。バックアップ取得にかかる時間に対し、リストアにあまりに多くの時間が取られる、という非生産的な制約ができてしまうので、上記の (1) の方法は断念することにしました。また上記の (1) ~ (3) 以外にも1件ずつ取り出して、1件ずつ書き込んで・・・という方法も考えられますが、同じ理由で非現実な方法となってしまうだけでなく、API の実行回数に比例するコスト上昇という問題も抱えてしまいます。

というわけで、(2) か (3) の方法での(一回の API 実行でまとめて書き込むような方法での)実装が候補として考えられるのですが、今回自分は (3) の方法を取ることにしたのでした。つまりバックアップ時にデータ取得 API を使って一括取得したデータをそのまま保存するのではなく、リストア用(データ書込み API 用)に加工してからバックアップファイルとして保存する、という方法です。そしてリストア時にはまずリストア先のデータベースを作成(既存の場合は削除した上で作成)した上で、取得したバックアップファイルをそのままデータ書込み API に渡して一括書込みを行うことにします。
バックアップツールの処理
  • データ取得 API を使って、バックアップ元データベースから全文書を一括取得
  • 取得した結果をデータ書込み API で一括書込みできるようなフォーマットに変換してから保存
リストアツールの処理
  • リストア先に指定されたデータベースが既に存在していたら削除する
  • リストア先データベースを作成
  • データ書込み API でバックアップファイル内の全文書データを一括書込み

なお、この方法であれば Node.js のような非同期処理の実行環境でも(一括で書込みを行うだけなので)あまり意識せずに実装することができます。

また、この (3) の方法であれば (2) の方法に比べてリストア時にはこのツールを使わなくても、バックアップファイルを指定して(curl などで)データ書込み API を直接実行してもリストアできる、という副産物的なアドバンテージもあります。


と、まあ色々を事情や背景を書き並べましたが、要は「自分はバックアップを手元に置きたい派」で、「自分で手軽にバックアップを取りたくて作った」のです。で、色々調べているうちに Cloudant API の制約事項の勉強にもなったし、一応動くものもできたし、いい勉強にもなりました。


マンホールマップに「スマホからもっと簡単に投稿できるようにしたい」という要望に応える新機能を用意しました。具体的には Twitter から投稿可能にしました。というわけで、以下の機能を使う前提として、スマホに Twitter アプリが導入されている必要があります。


また、この機能を使うには、Twitter で @Manholemap_Bot をフォローしてください( #manhotalk_bot と似ていてややこしいですが間違えないでくださいw)。この機能のために作成した新しいボットのアカウントです:
2017030801


試しに三鷹のこのマンホールを投稿してみることにします。この画像がスマホの中に保存されているものとします:
mitaka


お持ちの各種スマホ(やPC)から、フォローした上記アカウントへのメンションでマンホール画像を送付してください。メンションとはメッセージの頭に @Manholemap_Bot (大文字小文字は区別しないので、全て小文字でもOKです)を付けて、画像を添付して投稿してください:
IMG_0365


基本的にスマホ側での作業はこれだけで投稿できます。以下はPCでの作業を想定しています。少し(最大5分)待つと、投稿した画像がマンホールマップに反映されます:
2017030802


投稿した本人(と同じ Twitter アカウントでログインした状態)がその画像ページにアクセスした場合は編集ボタンが表示され、投稿の編集が可能になります:
2017030803


位置やテキストなど、必要に応じて編集して、最後に「更新」します:
2017030804


残念ながらまだいくつかの制約事項があります:
(1) テキストを同時にツイートできない
(2) 元の画像に位置情報が含まれていても反映されない(Twitter の仕様)

色々調査しながらにはなりますが、今後のアップデートで少しずつ便利にしていくつもりです。


なお、この機能はアプリケーション開発者向けに公開しているマンホールマップ API を使って作成したものです。誰でも使えるものなので、興味をお持ちの方はこちらから仕様書をどうぞ:
http://manholemap.juge.me/dev.jsp



IBM ワトソンなどの各種コグニティブエンジンを使う際に、重要なのは学習データだと思っています。

要はコグニティブエンジンを使って問い合わせを行うわけですが、その問い合わせする前に何らかの学習をする必要があり、そこで何をどれだけ学習させたかによって問い合わせの精度が変わってくるからです。

で、先日こんなツイートをしました:
2017022601

https://twitter.com/dotnsf/status/834959690803007488


↑は、その学習データを何らかの方法で集める際に、(非同期処理で集めるのではなく)マルチスレッド処理がいいのではないか、と思ってつぶやいたのでした。このことをもう少し詳しく紹介しようと思います。なお、あくまで特定条件下での個人見解なので他の人の意見も聞いてみたいと思ってます。

やろうと思っているのは、大まかにはこんな内容です:
  1. あらかじめ用意したインターネット上の URL のリストから、その HTML を取得して学習データにしたい
  2. この「リスト」が大量にあるケースを想定する
  3. なお、リストの中には実在しない URL が存在している可能性がある(つまりリストそのものが間違っている可能性がある)
  4. また URL は実在しているのだが、ネットワーク障害や DNS の設定ミスなど何らかの原因でアクセスする際に非常に長い時間がかかる(長い時間がかかった結果、タイムアウトになったり成功したりする)場合もある

1と2はごく普通の条件ですよね。ここではインターネット上の URL のリストは配列変数で用意されているものとしましょう。配列から1つずつ URL を取り出して、その URL の HTML を取得する、という処理を施すことになります。

問題は3と4の条件です(でも現実的に想定すべき条件だと思ってます)。3は用意されたリストそのものが間違っているというケース、つまり与えられた URL は "Unknown Host" になったり、404 などのエラーが返ってくることを想定しないといけない、ということです。

また4は更にややこしい条件です。成功するかしないかではなく、こちらからは手が出せない箇所のなんらかの障害によって目的の URL へのアクセスに非常に時間がかかってしまうケースです。時間がかかった結果、目的の HTML が取得できればいいのですが、最終的にタイムアウトエラーが発生することもあり得る、というケースを想定する必要があります。


要するにこれらを想定したエラー対策が必要になるのですが、まずは3と4を無視して(エラーが発生しない前提で)普通にアルゴリズムを考えるとこんな感じになるでしょうか:
//. URL の配列
urls = [
  "http://xxx.com/a1.html",
  "https://xxx.com/a2.html",
  "https://yyy.net/",
  "http://abc.com/xyz.html",
     :
     :
];

//. URL を1つずつ取り出して HTML を取り出す
forall( url in urls ){
  html = getHTML( url );
      :
      :
  (取り出した HTML を使って学習処理を行う)
      :
      :
}


↑の例ですと、getHTML 関数の中で実際に指定した URL にアクセスして HTML を取り出す、という処理をするものとします。そしてこの関数の中で3や4を原因とするエラーや時間がかかるといった現象が発生することを想定してみます。

3のケースは実は単純で、実在しない URL が指定された場合はこの関数の返り値を null などにして、null でなかった場合のみ処理を続ける、という判断を加えることで解決します:
//. URL の配列
urls = [
  "http://xxx.com/a1.html",
  "https://xxx.com/a2.html",
  "https://yyy.net/",
  "http://abc.com/xyz.html",
     :
     :
];

//. URL を1つずつ取り出して HTML を取り出す
forall( url in urls ){
  html = getHTML( url );
  if( html != null ){ //. 3の対策
      :
      :
  (取り出した HTML を使って学習処理を行う)
      :
      :
  }
}


さて問題は 4 のケースです。3 のアルゴリズムを単純に(シーケンシャルに)実行した場合、特定の URL から HTML を取り出す処理に時間がかかってしまうような事態が発生すると、リストの途中で処理が先に進まなくなってしまう、ということになります。このようなケースを想定した上で効率よく処理を実行するにはどう改良すべきでしょうか?

1つの方法として考えたのが非同期処理です。上記のループ部分を非同期に処理して、リスト内の各 URL へのアクセスを非同期に行う、という方法です。アルゴリズムにするとこのような感じになるでしょうか:
//. URL の配列
urls = [
  "http://xxx.com/a1.html",
  "https://xxx.com/a2.html",
  "https://yyy.net/",
  "http://abc.com/xyz.html",
     :
     :
];

//. URL を1つずつ取り出して HTML を取り出す
forall( url in urls ){
  html = getHTML( url, function( err, html ){  //. getHTML を非同期に実行する
    if( err ){
      //. 3. のエラー処理
    }else{
//. 成功した場合
: : (取り出した HTML を使って学習処理を行う) : : } }); }
↑非同期ということで JavaScript っぽくしてみました

このように非同期に処理を行うことで、「各 URL の HTML を取得する」という命令を全て先に実行しておき、(成功にせよエラーにせよ)取得の処理が終わったらそれぞれの続きを行う、というロジックです。こうすることで 4 のような事態が発生しても、正常な URL に対する処理は邪魔されることなく先に終了し、時間のかかる処理だけが後から実行される、という一見きれいな形になります。

しかし、この方法にも問題点がありました。それは URL のリストが膨大だった場合です。上記のコードが非同期に実行されるとまず全て URL に対する HTML 取得のリクエストが発行されます。そしてその処理はシステムのメモリ量や TCP ソケット数上限を超えて実行されてしまう可能性があります。この部分はコーディングというよりもシステムのメモリ管理やソケット数管理などの厄介そうな処理を行う必要がでてきてしまいます。

で、冒頭のマルチスレッドです。上記の非同期で行っていた部分をマルチスレッドに書き換えることで、(スレッド生成間のスリープ時間を調整するなどの)ある程度のスケジュール調整をした上で同時に HTML を取得する処理を行うことができるようになります。例えばこんな感じです(ここは言語依存がありそうなので、Java 丸出しで記述しています):
//. URL の配列
urls = [
  "http://xxx.com/a1.html",
  "https://xxx.com/a2.html",
  "https://yyy.net/",
  "http://abc.com/xyz.html",
     :
     :
];

//. URL を1つずつ取り出して HTML を取り出す
forall( url in urls ){
  //. 子スレッドを生成して、スレッドの中で取得処理を実行
  Download dl = new Download( url );
  Thread t = new Thread( t );
  t.start();

  try{
    Thread.sleep( 1000 ); //. 1秒待ってから次の URL を処理
  }catch( Exception e ){
  }
}


//. マルチスレッドで動くインスタンス
public class Download implements Runnable{
  private String url = "";
  public Download( String url ){
    this.url = url;
  }

  public void run(){
    html = getHTML( url );
    if( html != null ){
      :
      :
  (取り出した HTML を使って学習処理を行う)
      :
      :
    }
  }
}

↑マルチスレッドなので Java 全開の記述法で

この方法であれば、各 URL に対する HTML の取得は別々のスレッドで行われるため、特定の URL で 4 のような遅延現象が発生しても、他の URL に対する処理がその遅延に巻き込まれることはありません。また、この例では1秒(1000ミリ秒)おきにスレッドを生成するようにしています。例えば1スレッドの処理が2秒程度かかるようなケースであっても、3つ目のスレッドが生成されたタイミングで最初のスレッドの処理は終了している可能性が高くなり、同時に実行されるスレッドの数がさほどは増えないようなアルゴリズムになっています。仮に 4 のようなケースが発生したとしても、そのスレッドだけはしばらくの間生き続けることになりますが、他のスレッドは生成しては処理されて消えてゆくという流れになるので、やはりシステムの限界を意識するような処理にはなりにくいアルゴリズムになっているのでした。


という所まで作っての「マルチスレッド処理が効率よい」という冒頭の結論に至ったわけでした。要するにこれなら深く考えずに作って動かしてもややこしい対応が必要になる心配が少ないかな、と。またマルチスレッド処理となると Java が得意とする処理ロジックであり、オッサン脳の出番が増えるかもしれないなあ、と感じたのでした。

このページのトップヘ