Published Date : 2019年12月31日21:58

p5.jsとnumjsで自然言語処理 - 類似単語のランキング
Natural Language Processing with p5.js and numjs - Ranking of similar words


This blog has an English translation



p5.jsとnumjsによる自然言語処理シリーズです。

A natural language processing series with p5.js and numjs.


今回の記事は、p5.jsとnumjsを使ったコサイン類似度による、類似単語のランキングを作りたいと思います。

In this article, I'll create a ranking of similar words by cosine similarity using p5.js and numjs.

もちろん、P5はJavascriptで作られているので、P5内でJavascriptそのものが使えます。 なので、基本P5で書いていきますが、P5では難しい文字列処理等はJavascriptでそのまま書いていきます。

Of course, p5.js is written in Javascript, so you can use it directly in p5.js. I usually write code in p5.js, but there many things that are difficult to process in p5.js (String operations, etc.), so if I encounter such a situation, I write code directly in Javascript.


目次

Table of Contents



概要
Summary


簡単な概要です。

Here's a quick overview.

まずこの記事の趣旨は、 NumjsというNumpy(Python)のJavascriptヴァージョンを使用して、 ML.js等のライブラリを使わずにできるだけ1から自然言語処理の仕組みを理解することです。

First of all, the purpose of this blog post is use Numjs that a Javascript version of Python's Numpy, to understand how natural language processing works, without using libraries like ML.js.

機械学習用のライブラリを使えば簡単に事は進みますが、少しでも細かくその仕組みを知っておけば、色々なことに応用できると思います。

If you use a library for machine learning, things go easy, but I think if you know how the details of how machine learning works, you can apply it to many things.

今回はこの記事で簡単に説明したコサイン類似度を使って、似ている単語のランキングが表示できるようなものを作りたいと思います。

This time, I'll use the cosine similarity I briefly described in this blog post to create something that can display the ranking of similar words.

類似単語のランキングを作成する簡単な全体の流れを下の図に示します。

I'll show you a simple overall flow for creating similar word rankings using the following diagram.


Responsive image




類似単語のランキング
Ranking of similar words



それでは単語の類似のランキングを作る前に、軽くコサイン類似度をおさらいしておきましょう。

Before we create a similar ranking of words, let's briefly review the cosine similarity.

以下は以前の記事に書いてある通りの内容です。

The following sentence is exactly what I wrote in this previous my blog post.

コサイン類似度を計算する式は以下の通り。

The formula for calculating cosine similarity is as follows.


Responsive image

コサイン類似度とは、簡単に言うと、2つのベクトルが完全に同じ方向を向いていれば「1」、逆向きであれば「-1」という数字を計算します。

Briefly, the similarity of the cosines is calculated as the number 1 if the two vectors are pointing in exactly the same direction, or the number -1 if they are pointing in the opposite direction.

より直感的に理解するためには、下に示した図の通り、単語をベクトルとして表現した場合の矢印の方向が、コサイン類似度を表している。

For a more intuitive understanding, as shown in the figure below, the direction of the arrow when a word is expressed as a vector represents the cosine similarity.


Responsive image

それではまず与えられた単語から、似ている単語のランキングを作成するスクリプトを書いていきましょう。

Let's start with a script that creates a ranking of similar words from given words.

作業フォルダの中身は以下のようになっています。

The contents of the working folder are as follows.

working directory
your working directory
    -- assets
        -- sixLittleMice.txt
    -- nlpExample.html
    -- nlpExample.js
    -- utils
        -- preprocess.js
        -- buildCoOccurrenceMatrix.js
        -- cosSimilarity.js
+       -- rankingOfWords.js

単語の類似ランキングを作る関数「rankingOfWords.js」を作成しましょう。

Let's create a function [rankingOfWords.js] that creates a similar ranking of words.

rankingOfWords.js
const descendingOrder = function(a,b) {
    if(a > b) return -1;
    if(b < a) return 1;
    return 0;
}

function rankingOfWords(query, w2id, id2w, vSize, cmat, rankingLimit = 10) {
    let similarWordList = [];

    if (!(query in w2id)){
        print("This word '" + query + "' is not in the list.");
        return;
    }

    let qid = w2id[query];
    let qvec = cmat.tolist()[qid];
    
    let similarityList = nj.zeros([vSize]);

    for (let i = 0; i < vSize; i++){
        let x = nj.array(cmat.tolist()[i]);
        let y = nj.array(qvec);
        similarityList.set(i, cosSimilarity(x,y).get(0));
    }

    let count = 0
    for (let [index, value] of similarityList.tolist().sort(descendingOrder).entries()){
        if (id2w[index] == query){
            continue;
        }
            
        similarWordList.push([id2w[index], value]);

        count += 1
        if (count >= rankingLimit) {
            return similarWordList;
        }
            
    }
}
nlpExample.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- p5.js cdn -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/p5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/addons/p5.dom.min.js"></script>
    <!-- numjs cdn -->
    <script src="https://cdn.jsdelivr.net/gh/nicolaspanel/[email protected]/dist/numjs.min.js"></script>
    <!-- utils -->
    <script src="../../utils/preprocess.js"></script>
    <script src="../../utils/buildCoOccurrenceMatrix.js"></script>
    <script src="../../utils/cosSimilarity.js"></script>
    <script src="../../utils/rankingOfWords.js"></script>
    <!-- sketch -->
    <script src="nlpExample.js"></script>
    <title>Document</title>
</head>
<body>
</body>
</html>

試しに使ってみましょう。

Let's try it.

nlpExample.js
let result;

function preload(){
    result = loadStrings("./assets/sixLittleMice.txt");
}

let word2Id;
let id2Word;
let corpus;

let arrOfResults;
let vocabSize;

let coOccurenceMatrix;

let arrayOfRanking = [];

function setup(){
    arrOfResults = preprocess(result);

    createCanvas(600,600)

    corpus = arrOfResults[0];
    word2Id = arrOfResults[1];
    id2Word = arrOfResults[2];

    vocabSize = Object.keys(word2Id).length;
    coOccurenceMatrix = createCoMatrix(corpus, vocabSize);
    
    let testWord = 'i';
    arrayOfRanking = rankingOfWords(testWord, word2Id, id2Word, vocabSize, coOccurenceMatrix);
    print('Similarity of words : ' + testWord + '\n' + arrayOfRanking);
}
Similarity of words : i
six,0.9999999999999996,little,0.4999999999999998,mice,0.3535533905932737,sat,0.28867513459481287,down,0,to,0,spin,0,pussy,0,cat,0,passed,0

ではユーザーの操作で類似度ランキングがブラウザ上に表示されるようにしてみましょう。

Let's make the similarity rankings appear in the browser by user interaction.

nlpExample.js
let result;

function preload(){
    result = loadStrings("./assets/sixLittleMice.txt");
}

let word2Id;
let id2Word;
let corpus;

let arrOfResults;
let vocabSize;

let coOccurenceMatrix;

let arrayOfRanking = [];
let input, button, notfound;
let placeHolder = [];
let description = "";
let rankingLimit = 10;

function setup(){
    arrOfResults = preprocess(result);

    createCanvas(600,600)

    corpus = arrOfResults[0];
    word2Id = arrOfResults[1];
    id2Word = arrOfResults[2];

    vocabSize = Object.keys(word2Id).length;
    coOccurenceMatrix = createCoMatrix(corpus, vocabSize);

    // gui
    input = createInput();
    input.position(20, 65);

    button = createButton('create');
    button.position(input.x + input.width, 65);

    notfound = createElement('h3', '').position(20, 100);

    description = createElement('h4', '').position(20, 100);

    for (let i=0; i < 10; i++){
        placeHolder.push(createElement('p', '').position(20, 150 + (i * 30)));
    }

    button.mousePressed(findSimWords);

    createElement('h2', 'Do you want to create a word similarity ranking?').position(20, 5);

    textAlign(CENTER);
    textSize(50);
}


function findSimWords() {
    notfound.html('');
    description.html('');
    placeHolder.forEach(function (element){
        element.html('');
    });

    let testWord = input.value();
    arrayOfRanking = rankingOfWords(testWord, word2Id, id2Word, vocabSize, coOccurenceMatrix, rankingLimit);
    input.value('');

    if (typeof arrayOfRanking == "undefined"){
        notfound.html("This word '" + testWord + "' is not in the list.").position(20, 100);
        return; 
    }
    
    if ((typeof placeHolder != "undefined") || (placeHolder != [])) {
        description.html('It is the best ' + rankingLimit + ' words ranking similar to ' + testWord + '.\n');
        for (let [index, arr] of arrayOfRanking.entries()) {
            placeHolder[index].html((index + 1) + ' : ' + arr[0] + " : " + Math.floor(arr[1] * 100) + '%' + '\n');
        }
    }
}

動画にて結果をお見せします。

I will show you the result in the video.



いくつか細かい部分を説明して終わります。

I will finish by explaining some details.

rankingOfWords.js
const descendingOrder = function(a,b) {
    if(a > b) return -1;
    if(b < a) return 1;
    return 0;
}

この関数は降順に配列の要素を並び替えるためにあります。 sortメソッドと組み合わせて使います。

This function sorts the elements of an array in descending order. Used with the sort method.

for (let [index, value] of similarityList.tolist().sort(descendingOrder).entries()){
    if (id2w[index] == query){
        continue;
    }

配列の後ろから一つずつ順番に数を比較して、大きいものを配列の先頭にもっていきます。 次にまた同じことを配列の先頭以外で繰り返します。 さらに次は配列の先頭と二番目以外を抜かして繰り返す。 さらに次は配列の先頭と二番目と三番目以外を抜かして繰り返す。 この作業を最後まで繰り返せば降順に整数が並ぶという訳です。

Compares the numbers one at a time from the end of the array and places the larger number at the beginning of the array. Then it repeats the same thing again except at the beginning of the array. Then, next step is to skip the first and second parts of the array, Then, next step is repeated except for the first, second, and third parts of the array. So if you repeat this process all the way to the end of the array, the integers are sorted in descending order.


Responsive image


Responsive image


Responsive image


Responsive image


Responsive image


Responsive image


Responsive image


Responsive image

次にGUI部分の説明です。

The following is a description of the GUI portion.

description = createElement('h4', '').position(20, 100);
for (let i=0; i < 10; i++){
   placeHolder.push(createElement('p', '').position(20, 150 + (i * 30)));
}

createElement methodで指定したHTMLの要素が作られます。 そしてposition methodで指定した場所へ配置されます。 今回は何度も表示させたり消したりする必要があるので、 先にプレースホルダーとして空の要素を作っておきます。

The HTML element specified by "createElement method" is created. It is then positioned at the location specified by "position method". This time, it needs to be displayed and deleted many times. So, I decided to first create an empty element as a placeholder.

button.mousePressed(findSimWords);

そして、[Create]ボタンが押されたら単語の類似ランキングを作り、画面に描画させる関数を起動が起動します。

When the [create] button is pressed, a function is invoked that creates a similar ranking of words and draws them on the screen.

function findSimWords() {
    notfound.html('');
    description.html('');
    placeHolder.forEach(function (element){
        element.html('');
    });

まず初めに、結果を表示させるHTML要素を初期化します。

First, we initialize the HTML element to display the results.

    let testWord = input.value();
    arrayOfRanking = rankingOfWords(testWord, word2Id, id2Word, vocabSize, coOccurenceMatrix, rankingLimit);
    input.value('');

次に、「createElementメソッド」で作成された入力要素から単語を受け取り、単語を「rankingOfWords関数」に渡して、その出力結果を受け取ります。

It then takes the word from the input element created in "createElement method" and passes the word to "rankingOfWords function", and receives the output.

    if (typeof arrayOfRanking == "undefined"){
    notfound.html("This word '" + testWord + "' is not in the list.").position(20, 100);
    return; 
}

出力結果が何もなければ、該当する単語が無い、もしくはエラーなので、その内容を表示させて終了します。

If there is no output result, there is no corresponding word or it is an error, so it displays the content and ends.

    if ((typeof placeHolder != "undefined") || (placeHolder != [])) {
        description.html('It is the best ' + rankingLimit + ' words ranking similar to ' + testWord + '.\n');
        for (let [index, arr] of arrayOfRanking.entries()) {
            placeHolder[index].html((index + 1) + ' : ' + arr[0] + " : " + Math.floor(arr[1] * 100) + '%' + '\n');
        }
    }
}

出力結果があるなら、「entriesメソッド」を使って作っておいた空のP要素に値を代入して表示させていきます。

If you have an output, you can display it by assigning a value to an empty P element created using "entries method".

次回もnumjsとP5を活用して何かやりたいと思います。Pythonになるかもね。

Next time, also I want to do something interesting with numjs and P5. But I might go back to Python.





See You Next Page!