JavaScript
Node.js
Azure
node-red
CognitiveServices

Steve Jobsが卒業スピーチで「大事な事だから2度以上言った単語」を自然言語処理で取り出す

More than 1 year has passed since last update.

 Node-REDは、機能毎に用意された処理ノードをマウスで繋ぐ操作で、簡単にアプリケーションを作成できる開発環境です。当初はIoT向けに開発されましたが、ウェブアプリケーション開発等、幅広い用途で利用されるようになりました。

 今回は自然言語処理の簡単な例として「大事な事だから2度以上言われた単語」を取り出す処理を実装します。


実装する処理

 自然言語処理をよく知っている方は、tf-idfの考え方と言うとイメージしやすいと思います。tf-idfは、検索エンジンが適切な文書が検索結果の上位に来るようソートする時に使うアルゴリズムで、検索キーワード中の特徴的な単語を用いてスコアリングする手法です。

 本実装ではテキストを入力とし、「文書中の単語の使用回数」を「単語の珍しさ」で割った値をスコアとして用いて、テキストの中で良く使われる重要な単語を取り出します。正確には「大事な単語で、かつ何度も言われた単語を取り出す」処理です。


「文書中の単語の使用回数」を求める

 「文書中の単語の使用回数」は、前回記事で紹介したワードカウントと同じ処理を用います。下記の様にテキストデータを含むinput.txtノードから順に処理を行い、「文書中の単語の使用回数」と「単語」のペアを出力します。

wordcount_nodered.png


「単語の珍しさ」を求める

 「単語の珍しさ」を求めるには、Microsoft Researchが研究向けに提供しているN-gram検索APIであるMicrosoft Web N-gram Servicesを使用します。本サービスは、ウェブでクロールしたデータから作成した大量(PBクラス)のテキストデータから生成したN-gramのデータを検索できるサービスです。本APIの詳細は、REST API連携の記事で紹介しています。出力値は対数値で直観的ではありませんが、大きい値ほどよく使われる単語、小さい値ほどあまり使われない単語となります。

※注: 2016年10月現在、本APIはAzureで提供されているCognitive ServicesのWeb Language Model APIとなり、商用で使えるようになりました。仕様が変更されたため、本記事の方法はそのまま用いることができませんので、ご注意ください。新しい仕様での使用方法は別記事「2分で実装!Node-REDで認証付きREST API呼び出し」で説明しました。


「大事な事だから2度以上言われた単語」を取り出す

 処理を実現するのに必要な2つの値が揃いましたので、「大事な事だから2度以上言われた単語」を抽出する処理を実装します。「文書中の単語の使用回数」を求める処理は、そのまま用いることができますが、「単語の珍しさ」を求める処理は各単語毎に問合せを行う必要があります。このような繰り返し処理が必要となるケースではデザインパターンで紹介したwhileパターンを用います。whileパターンは「8の字型の接続」とその前の「ループの条件ノード」が目印です。

 「文書中の単語の使用回数」と「単語の珍しさ」の処理を組み合わせて単語をスコアリングする処理を実装したノードは下のとおりです。

tfidf.png

 次に実際に動かしてみます。テキストを格納するinput.txtには、スティーブジョブズの卒業スピーチのテキストを与えました。実行すると下のように左側にスコア、右側に単語が入った配列を出力します。

tfidfresult.png

 本スピーチを聞いたことがある方であれば、話の内容が蘇る単語が多く存在していることがお分かりいただけると思います。例えば、calligraphy(美しいフォントの技術)やtypography(活字の仕上がり)は、「マッキントッシュが美しいフォントを持った」話から来た単語です。また、dots(点)は「点が線で繋がった」話と一致します。自分の主張ポイントを明らかにするため、大事な事だから何度も言い、視聴者の記憶に単語が残るスピーチの仕方は、真似したいところです。

 ちなみにスコアを昇順で並び変えた場合の結果は下のようになります。一般的に文書内に存在する単語が、あまり文章で使われいない単語ほど上位に現れてきます。

tfidfresult2.png


最後に

 今回は、Node-REDを用いて少し賢い処理を実装してみました。次回も少し踏み込んだ自然言語処理を実装してみたいと思います。


ソースコード

 「N-gram問合せ」のノードにリクエストキーを追加し、テキストデータが格納されているinput.txtノードの内容を変更して遊んでみてください。

[{"id":"25dfda7e.da2026","type":"inject","z":"765ca782.89a358","name":"実行","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":125.37122344970703,"y":378.00006008148193,"wires":[["9e72b5fa.618d48"]]},{"id":"4087f8e3.bf7808","type":"function","z":"765ca782.89a358","name":"空白で区切り配列化","func":"msg.payload = msg.payload.split(/\\s+/);\n\nreturn msg;","outputs":1,"noerr":0,"x":139.37120819091797,"y":465.00006008148193,"wires":[["58645681.a79ba8"]]},{"id":"9354a4fb.6cab58","type":"function","z":"765ca782.89a358","name":"単語を辞書順でソート","func":"msg.payload = msg.payload.sort();\n\nreturn msg;","outputs":1,"noerr":0,"x":145.37120819091797,"y":560.000075340271,"wires":[["7a82ffc9.857d"]]},{"id":"7a82ffc9.857d","type":"function","z":"765ca782.89a358","name":"単語数をカウント","func":"var input = msg.payload;\nvar output = [];\nvar str_current = null;\nvar count = 0;\nvar i = 0;\n\ndo\n{\n    var str_temp = input[i];\n    if (str_current === null)\n    {\n        str_current = str_temp;\n        count++;\n    } else if (str_temp === str_current) {\n        count++;\n    } else {\n        output.push(count + \" \" + str_current);\n        str_current = str_temp;\n        count = 1;\n    }\n    i++;\n}\nwhile (i < input.length);\noutput.push(count + \" \" + str_current);\n\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":335.37117767333984,"y":560.0000905990601,"wires":[["74ac9b33.8b5364"]]},{"id":"58645681.a79ba8","type":"function","z":"765ca782.89a358","name":"大文字を小文字に変換","func":"var input = msg.payload;\nvar output = [];\n\nfor (var i = 0; i < input.length; i++)\n{\n    var temp = input[i];\n    output.push(temp.toLowerCase());\n}\n\nmsg.payload = output;\n\nreturn msg;","outputs":1,"noerr":0,"x":342.37120819091797,"y":465.00006008148193,"wires":[["d3bd232a.2c42e"]]},{"id":"74ac9b33.8b5364","type":"function","z":"765ca782.89a358","name":"単語数降順で単語をソート","func":"msg.payload = msg.payload.sort(function(a, b) {\n    return b.split(/ /)[0] - a.split(/ /)[0];\n});\n\nreturn msg;","outputs":1,"noerr":0,"x":538.3711776733398,"y":560.0000600814819,"wires":[["e806e41e.17f918"]]},{"id":"eac429f9.153bd8","type":"comment","z":"765ca782.89a358","name":"←cat input.txt相当","info":"","x":432.37121200561523,"y":378.1667528152466,"wires":[]},{"id":"b0ed9ca9.4f126","type":"comment","z":"765ca782.89a358","name":"↓tr A-Z a-z相当","info":"","x":346.37120819091797,"y":429.00006008148193,"wires":[]},{"id":"146c68fe.eb9397","type":"comment","z":"765ca782.89a358","name":"↑sed s/ /\\n/g相当","info":"","x":143.37120819091797,"y":502.00006008148193,"wires":[]},{"id":"7f15488d.80eab8","type":"comment","z":"765ca782.89a358","name":"↑sort相当","info":"","x":152.37120819091797,"y":596.0000600814819,"wires":[]},{"id":"a56e3414.5a91c8","type":"comment","z":"765ca782.89a358","name":"↓sort -k 1 -n -r相当","info":"","x":542.371208190918,"y":524.0000600814819,"wires":[]},{"id":"7e42236e.81bddc","type":"switch","z":"765ca782.89a358","name":"i!=-1","property":"i","rules":[{"t":"neq","v":"-1"},{"t":"else"}],"checkall":"true","outputs":2,"x":105.64900588989258,"y":863.7965666055679,"wires":[["2fc07388.d03f8c"],["b877ed1b.47881"]]},{"id":"56fad613.a90528","type":"function","z":"765ca782.89a358","name":"i--","func":"msg.i--;\n\nreturn msg;","outputs":1,"noerr":0,"x":481.14893341064453,"y":744.185440659523,"wires":[["7e42236e.81bddc"]]},{"id":"ed94f505.126b08","type":"comment","z":"765ca782.89a358","name":"awk '2<=$1'相当","info":"","x":168.7045440673828,"y":718.3333864212036,"wires":[]},{"id":"16d0b363.e92f4d","type":"function","z":"765ca782.89a358","name":"i=payload.length-1等","func":"msg.tmp = msg.payload;\nmsg.i = msg.payload.length-1;\nmsg.data = new Array(msg.payload.length);\n\nreturn msg;","outputs":1,"noerr":0,"x":156.51154327392578,"y":780.6858068704605,"wires":[["7e42236e.81bddc"]]},{"id":"2fc07388.d03f8c","type":"function","z":"765ca782.89a358","name":"payload=tmp[i]等","func":"msg.freq = msg.tmp[msg.i].split(/ /)[0];\nmsg.word = msg.tmp[msg.i].split(/ /)[1];\nmsg.payload = msg.tmp[msg.i].split(/ /)[1];\n\nreturn msg;","outputs":1,"noerr":0,"x":255.8512725830078,"y":857.6781822443008,"wires":[["5a969781.a56968","50632d27.af9cd4"]]},{"id":"47916a19.b86e94","type":"function","z":"765ca782.89a358","name":"tfidf算出等","func":"var df = new Number(msg.payload);\nvar tfidf = msg.freq / Math.pow(2, df);\nmsg.data[msg.i] = tfidf + \" \" + msg.word;\n\nreturn msg;","outputs":1,"noerr":0,"x":732.6489601135254,"y":855.3054332733154,"wires":[["56fad613.a90528"]]},{"id":"efac834.f10538","type":"debug","z":"765ca782.89a358","name":"スコア出力","active":true,"console":"false","complete":"payload","x":727.0656509399414,"y":895.0276705026627,"wires":[]},{"id":"5a969781.a56968","type":"http request","z":"765ca782.89a358","name":"N-gram問合せ","method":"GET","ret":"txt","url":"http://weblm.research.microsoft.com/rest.svc/bing-body/2013-12/4/jp?u=<トークン>&p={{payload}}&format=json","x":429.7878952026367,"y":856.555560708046,"wires":[["62aecb88.9d5134"]]},{"id":"62aecb88.9d5134","type":"change","z":"765ca782.89a358","name":"headers削除","rules":[{"t":"delete","p":"headers"}],"action":"","property":"","from":"","to":"","reg":false,"x":582.5656433105469,"y":856.3887726068497,"wires":[["47916a19.b86e94","efac834.f10538"]]},{"id":"b14837e.f4eb7c8","type":"debug","z":"765ca782.89a358","name":"ifidfスコアリング結果","active":true,"console":"false","complete":"payload","x":507.2045593261719,"y":943.4999805688858,"wires":[]},{"id":"50632d27.af9cd4","type":"debug","z":"765ca782.89a358","name":"問合せる単語出力","active":true,"console":"false","complete":"payload","x":442.4545135498047,"y":896.6666485071182,"wires":[]},{"id":"57f032fe.a80fcc","type":"function","z":"765ca782.89a358","name":"頻度9以下の単語を選択","func":"var input = msg.payload;\nvar output = [];\n\nfor (var i = 0; i < input.length; i++)\n{\n    var temp = input[i];\n    var count = temp.split(/ /)[0];\n    if (count <= 9)\n    {\n        output.push(temp);\n    }\n}\n\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":409.2878723144531,"y":682.000075340271,"wires":[["16d0b363.e92f4d"]]},{"id":"d3bd232a.2c42e","type":"function","z":"765ca782.89a358","name":"英字から成る単語のみ選択","func":"var input = msg.payload;\nvar output = [];\n\nfor (var i = 0; i < input.length; i++)\n{\n    var temp = input[i];\n    if (temp.match(/^[a-z]+$/))\n    {\n        output.push(temp);\n    }\n}\n\nmsg.payload = output;\n\nreturn msg;","outputs":1,"noerr":0,"x":567.121208190918,"y":464.50006008148193,"wires":[["9354a4fb.6cab58"]]},{"id":"c67eee89.39811","type":"comment","z":"765ca782.89a358","name":"↓grep -E \"^[a-z]+$\"相当","info":"","x":568.121208190918,"y":427.50006008148193,"wires":[]},{"id":"181f9d0d.e7e063","type":"comment","z":"765ca782.89a358","name":"↑uniq -c相当","info":"","x":335.12120819091797,"y":595.5000600814819,"wires":[]},{"id":"e806e41e.17f918","type":"function","z":"765ca782.89a358","name":"単語数2以上の単語を選択","func":"var input = msg.payload;\nvar output = [];\n\nfor (var i = 0; i < input.length; i++)\n{\n    var temp = input[i];\n    var count = temp.split(/ /)[0];\n    if (2 <= count)\n    {\n        output.push(temp);\n    }\n}\n\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":164.62120056152344,"y":681.0000638961792,"wires":[["57f032fe.a80fcc"]]},{"id":"9e72b5fa.618d48","type":"template","z":"765ca782.89a358","name":"input.txt","field":"payload","format":"handlebars","template":"This is a pen.\nGive me a pen.\nThis pen is small.","x":259.850341796875,"y":377.50000381469727,"wires":[["4087f8e3.bf7808"]]},{"id":"85108454.7aef78","type":"comment","z":"765ca782.89a358","name":"awk '9<=$1'相当","info":"","x":410.36769104003906,"y":643.9815788269043,"wires":[]},{"id":"b877ed1b.47881","type":"function","z":"765ca782.89a358","name":"スコア降順で単語をソート等","func":"msg.payload = msg.data;\n\nmsg.payload = msg.payload.sort(function(a, b) {\n    return b.split(/ /)[0] - a.split(/ /)[0];\n});\n\nreturn msg;","outputs":1,"noerr":0,"x":281.1818313598633,"y":943.8181705474854,"wires":[["b14837e.f4eb7c8"]]}]