JavaScript
Node.js
NLP
Bluemix
node-red

Node-REDでカンタン!自然言語処理

More than 1 year has passed since last update.

GUIで簡単にプログラムを開発できるNode-REDは、元々IoT向けに作られた開発環境です。しかしIoTの範疇に留まらず、のちにウェブアプリケーション等さまざまな用途で活用されるようになりました。その中でも、私は特に自然言語処理との相性が良いと思います。その相性の良さについて、最も基本的な自然言語処理であるワードカウントを例に説明します。

まず、Node-REDを用いたワードカウントの説明の前に、従来手法を2つ説明します。


Linuxコマンドでワードカウント(従来手法1)

テキストファイルが手元にあり、単語を使用頻度順に並べたい場合、どのように実装するのが効率が良いでしょうか。Linuxのコマンドを知っている方は、下記の様なコマンドを用いると思います。

cat input.txt | sed "s/ /\n/g" | tr A-Z a-z | grep -E "^[a-z]+$" | sort | uniq -c | sort -k 1 -n -r | head

出力結果:

98 the
85 i
71 to
66 and
47 was
46 a
45 it
41 of
38 that
34 in

出力結果からテキストファイルinput.txtではtheが98回、iが85回、toが71回使われていることが分かります。各コマンドの説明は下記のとおりです。

コマンド
説明

cat input.txt
テキストファイルを読み込む

sed "s/ /\n/g"
空白区切りで改行

tr A-Z a-z
大文字を小文字に変換

grep -E "^[a-z]+$"
英字からなる単語のみ選択(その他は除外)

sort
単語をアルファベット順でソート

uniq -c
隣り合う行に同じ単語が存在する場合、重複をなくし1行にする。(同時に単語数をカウント)

sort -k 1 -n -r
単語数(1列目)を数値として降順でソート

head
先頭の10行のみ表示

Linuxコマンドを用いる方法は、Linux環境があれば、すぐに処理を実装できます。また、試行錯誤を繰り返す開発を効率良く行える点も特徴です。例えば、ワードカウントの処理結果の末尾は殆ど単語数が1の単語ですので、後続の処理を実装する場合は、CPUリソースを節約するため、除くことが多いです。その場合はuniqコマンドに-dオプションを追加し、「uniq -c」を「uniq -c -d」とすることで、単語数が2以上の単語のみを出力できます。また、sortコマンドの-rオプションの有無で、ソートの降順昇順を切り替えることが可能です。このように、Linuxコマンドによる実装は僅か数文字の修正で、処理を書き換えることができ、試行錯誤を繰り返す開発に向いています。


Javaでワードカウント(従来手法2)

上手くLinuxコマンドでワードカウントを実装できました。次にこの処理を人に見せるため、ウェブアプリケーション化したい場合は、どのように実装すれば良いでしょうか。私はNode-REDを使うまで、Linuxコマンドで実装した処理を下記の様なJavaのコードとして書き直していました。


WordCount.java

import java.io.*;

import java.util.*;

public class WordCount {
public static void main(String[] args) throws Exception {
Map<String, Integer> hm = new HashMap<>(); // 単語と単語数の格納場所を用意
String line;
BufferedReader br = new BufferedReader(new FileReader(args[0])); // 第一引数で指定したテキストファイルを開く
while ((line = br.readLine()) != null) { // ファイルを1行ずつ読み込み
String[] strs = line.toLowerCase().split("\\s+"); // 小文字化し、空白で区切り配列化
for (String str : strs) // 1単語ずつループ
if (hm.containsKey(str)) { // 格納済みの単語であるか判定
int tmp = hm.remove(str); // 格納済みの単語の場合、一旦取り出し、
tmp++; // 単語数をインクリメント後、
hm.put(str, tmp); // 再度格納する。
} else
hm.put(str, 1); // 初回の単語なら単語数1を格納
}

ArrayList<Map.Entry<String, Integer>> li = new ArrayList<>(hm.entrySet()); // ソート用の格納場所を用意
Collections.sort(li, new Comparator<Map.Entry<String, Integer>>() { // 単語数を降順にソート
public int compare(Map.Entry<String, Integer> me1, Map.Entry<String, Integer> me2) { // 比較方法を定義する関数
return me2.getValue() - me1.getValue(); // 単語数で比較
}
});

for (int i = 0; i < 10 && i < li.size(); i++) { // 単語数上位10件を出力
Map.Entry<String, Integer> me = li.get(i); // 単語と単語数のペアを取り出す
System.out.println(me.getValue() + " " + me.getKey()); // 単語数と単語を出力
}
}
}


コマンド:

C:\Users\zuhito\Desktop>javac WordCount.java
C:\Users\zuhito\Desktop>java WordCount input.txt

出力結果:
98 the
85 i
71 to
66 and
47 was
46 a
45 it
41 of
38 that
34 in

実際はウェブアプリケーション化するため、さらにServletのプログラムにする必要があります。Javaを用いた実装は、移植性やセキュリティの面でLinuxコマンドより優れています。しかし、Linuxのコマンドの記述方法と、Javaのコードは全く記述方法が異なるため、この書き直し作業はとても大変です。大抵は、全く同じ実装にならず、Linuxコマンドの処理で評価した精度等の結果が異なってしまいます。


Node-REDでワードカウント(提案手法)

今回提案するNode-REDを用いた開発では「Linuxコマンドの様な試行錯誤を繰り返す開発」と「ウェブアプリケーション化」の両方を同時に実現できます。Node-REDでワードカウントを行うノードは、下の様になります。

wordcount_nodered.png

出力結果:

wordcount_result.png

各ノードはLinuxコマンドと同じ処理を記述してあります。そのため、一旦ノードを用意してしまえば、Linuxコマンドで行っていた試行錯誤を繰り返す開発をNode-RED上で行うことができるようになります。例えば、大文字と小文字を区別してワードカウントしたい場合は、「大文字を小文字に変換」のノードを削除し、「空白で区切り配列化」のノードから直接「英字から成る単語のみ選択」のノードに線を繋ぐだけで修正が完了します。また「単語数を降順でソートするノード」と、「昇順でソートするノード」を用意しておけば、ノードをつなぎ変えるのみで並び順を変更できます。

しかも、Node-REDはタブレットで修正操作ができるため、タブレットさえあれば出張で移動中の電車の中など、いつでもどこでも開発ができます。iPad、Android、WindowsタブレットからNode-REDにアクセスすると、下の様にフリック入力と似たUIで操作でき、とても便利です。

wordcount_ui.png

もちろん、Node-REDはウェブサーバ上で動作していますので、前回記事でご紹介したUI & APIパターンを用いて、すぐにウェブアプリケーション化できます。以上の様に、Node-REDを用いた開発は、2つの従来手法が不要になるため、大幅に開発時間を削減できます。


最後に

Node-REDにて処理をLinuxコマンド単位でカプセル化することで、効率良く開発できることをご理解いただけたと思います。ワードカウントだけでは面白みがないので、次回の記事では、もう少し踏み込んだ自然言語処理をご紹介します。お楽しみに。


ソースコード

Node-REDでワードカウントを行う処理のソースコードは下記です。本ソースコードはBluemix上のNode-REDはもちろん、ローカルPC上のNode-REDでも動作します。テキストデータが格納されているinput.txtノードの内容を変更して遊んでみてください。



[{"id":"25dfda7e.da2026","type":"inject","z":"765ca782.89a358","name":"WordCount実行","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":223.25001525878906,"y":70.5,"wires":[["6062dc2b.9f9d24"]]},{"id":"6062dc2b.9f9d24","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":374.2500305175781,"y":70.5,"wires":[["4087f8e3.bf7808"]]},{"id":"4087f8e3.bf7808","type":"function","z":"765ca782.89a358","name":"空白で区切り配列化","func":"msg.payload = msg.payload.split(/\\s+/);\n\nreturn msg;","outputs":1,"noerr":0,"x":237.25,"y":157.5,"wires":[["58645681.a79ba8"]]},{"id":"be86414d.4179c","type":"debug","z":"765ca782.89a358","name":"WordCount結果出力","active":true,"console":"false","complete":"payload","x":689.25,"y":342.5,"wires":[]},{"id":"9354a4fb.6cab58","type":"function","z":"765ca782.89a358","name":"単語を辞書順でソート","func":"msg.payload = msg.payload.sort();\n\nreturn msg;","outputs":1,"noerr":0,"x":243.25,"y":252.50001525878906,"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":433.2499694824219,"y":252.50003051757812,"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":440.25,"y":157.5,"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":636.2499694824219,"y":252.5,"wires":[["eecb5b85.1134a8"]]},{"id":"eac429f9.153bd8","type":"comment","z":"765ca782.89a358","name":"←cat input.txt相当","info":"","x":555.25,"y":71.5,"wires":[]},{"id":"b0ed9ca9.4f126","type":"comment","z":"765ca782.89a358","name":"↓tr A-Z a-z相当","info":"","x":444.25,"y":121.5,"wires":[]},{"id":"146c68fe.eb9397","type":"comment","z":"765ca782.89a358","name":"↑sed s/ /\\n/g相当","info":"","x":241.25,"y":194.5,"wires":[]},{"id":"7f15488d.80eab8","type":"comment","z":"765ca782.89a358","name":"↑sort相当","info":"","x":250.25,"y":288.5,"wires":[]},{"id":"a56e3414.5a91c8","type":"comment","z":"765ca782.89a358","name":"↓sort -k 1 -n -r相当","info":"","x":640.25,"y":216.5,"wires":[]},{"id":"51056add.aefa94","type":"comment","z":"765ca782.89a358","name":"head相当→","info":"","x":298,"y":343,"wires":[]},{"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":665,"y":157,"wires":[["9354a4fb.6cab58"]]},{"id":"c67eee89.39811","type":"comment","z":"765ca782.89a358","name":"↓grep -E \"^[a-z]+$\"相当","info":"","x":666,"y":120,"wires":[]},{"id":"eecb5b85.1134a8","type":"function","z":"765ca782.89a358","name":"上位10件のみ選択","func":"var input = msg.payload;\nvar output = [];\n\nfor (var i = 0; i < input.length && i < 10; i++)\n{\n output.push(input[i]);\n}\n\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":491,"y":343,"wires":[["be86414d.4179c"]]},{"id":"181f9d0d.e7e063","type":"comment","z":"765ca782.89a358","name":"↑uniq -c相当","info":"","x":433,"y":288,"wires":[]}]