アリソンパリッシュによる
このチュートリアルでは、ダニエルハウと貢献者チームによる、p5.jsと互換性のあるJavaScriptライブラリであるRiTaJSの機能のいくつかを示します。その過程では、オブジェクトとデータに関する事柄も学んでいきます。
RiTaJSは、テキストを操作し生成するための素晴らしいライブラリです。このチュートリアルで扱うのは、RiTaJSで行えることのほんの一部に過ぎないので、ぜひRiTaJSサイトのチュートリアルやさまざまなサンプルスケッチを試し、RiTaJSリファレンスに当たるようにしてください。
RiTaJSのインストールに助けが必要な場合には、このチュートリアルを参照してください。きっとうまくいくはずです。以降のすべてのサンプルは、スケッチにRiTaJSライブラリがインストールされていることを前提としています(インストールはスケッチごとに行う必要があります)。
目次
メモ
以降では、p5.jsエディタではなく、一般的なindex.html + sketch.jsで作業しています。作業フォルダにはindex.htmlとrita-full.min.js、sketch.jsが同一階層にあります。index.htmlでは次の<script>タグを記述します。
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.dom.min.js"></script>
<script src="rita-full.min.js"></script>
ワードクラウド… その簡単な方法
テキストデータを可視化する典型的なテクニックの1つは”ワードクラウド”で、Wordleなどのツールで作成されます。ワードクラウドはテキストを視覚化する良い方法ではありませんが、見栄えが良く、実装もかなり容易です。
ワードクラウドは次のように機能します。まずソーステキストを調べて、各単語が何回登場するかを数えます。それから各単語を画面に描画し、単語の頻度に応じてフォントサイズを変更します。RiTaJSが簡単にするにはこの作業の初めの部分です。RiTaJSに含まれるconcordance()関数はパラメータとして大きな文字列を取り、各単語を単語の登場回数にマッピングしたJavaScriptオブジェクトを返します。
初めの一歩:コンコーダンスと単語数
以下は、RiTaJSのconcordance()関数の働きを示すサンプルです。テキストファイルには好きなものが使用できます。サンプルでは欽定訳聖書第1章のプレーンテキスト版を使用しています。
let lines;
let counts;
let field;
let button;
function preload() {
// linesは行ごとの文字列を要素に持つ配列
lines = loadStrings('genesis.txt');
}
function setup() {
createCanvas(400, 300);
//console.log(lines);
// 行ごとの文字列を半角スペースで結合 => 全体が1つの文字列になる
const str = lines.join(' ');
//console.log(str);
// https://rednoise.org/rita/reference/RiTa/RiTa.concordance/index.php
// 与えられたテキストから、単語とその出現頻度のリストであるコンコーダンスを作成する。
// 戻り値;Object
counts = RiTa.concordance(str);
console.log(counts);
// UIを作成
field = createInput();
button = createButton("単語数を取得");
button.mousePressed(displayCount);
// 描画パラメータを設定
background(50);
textAlign(CENTER, CENTER);
textSize(24);
noStroke();
fill(255);
noLoop();
}
function draw() {}
// ボタンが押されたら
function displayCount() {
background(50);
// テキストボックスに入力された文字
const word = field.value();
let wordCount = 0;
if (counts.hasOwnProperty(word)) {
wordCount = counts[word];
}
text(wordCount, width / 2, height / 2);
}
このスケッチはテキストフィールドとボタンを表示します。ボタンをクリックすると、テキストフィールドに入力した単語がgenesis.txtに現れた回数が描画されます。この検索では大文字小文字が区別されます(Godでは32、godでは0になります)。
このスケッチにはいくつかの新出事項があります。1つめはRiTa.concordance()の呼び出しです。この関数はパラメータとして単一の文字列を取り、キーとして単語を持つオブジェクトを返します。各キーの値はその単語がテキストに登場した回数です。オブジェクトは次のデータ構造をしています。
特定の単語の登場回数は、式count[x]の評価で取得できます(xは調べたい単語)。このスケッチの”本質”は、ユーザーのクリックでp5.jsが呼び出すdisplayCount()関数にあります。この関数は、countsオブジェクトがテキストフィールドに入力された単語を含んでいるかどうかを調べ、含んでいる場合にはその単語の登場回数を表示し、含んでいない場合には0を表示します(単語がオブジェクト内で見つからないなら、テキスト内の登場回数は0なので、これは正確です)。
上記コードにはまた、これまで出て来なかった初めてのメソッド、.hasOwnProperty()があります。このメソッドは、与えられた値がオブジェクトにキーとして存在する場合にtrueを返し、存在しない場合にfalseを返します。上記コードでは、.hasOwnProperty()を使って、ユーザーが入力した単語が、concordanceオブジェクトに存在するかどうかを調べています。存在する場合には、その単語の値を取得し、存在しない場合には、デフォルト値の0にフォールバックします。
オブジェクトのキー/値のペアの繰り返し処理
このスケッチはよくできていますが、少なくともまだワードクラウドではありません。ワードクラウドにするには、1つや2つの単語ではなく、RiTa.concordance()が返すオブジェクトの全部の単語を表示する必要があります。これらの値(単語)をコードで決め打ちすることはできません。テキストにどんな一意の単語が現れるか予想できないからです(これはconcordance()関数を最初に使用する理由の1つです)。最も簡単なのは、オブジェクトが保持しているキー/値のペア全部に対して実行するコードの記述方法を見つけることです。しかし、どうやって?
JavaScriptでは、これを実現できる慣用的な方法として、forループを特殊なシンタックスで使用する次の方法があります。
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// コードをここに記述
console.log(key, obj[key]);
}
}
p5.jsエディタのウィンドウを空にして、次のコードを試してみましょう。
const obj = {'alpha': 1, 'beta': 2, 'gamma': 3};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
// コードをここに記述
console.log(key, obj[key]);
}
}
コンソールの出力領域には次の結果が表示されます。
このコードはテンプレートのようなものとして使用できます。そのときには、3箇所あるobjを、繰り返し処理を行いたいオブジェクトの変数名に置き換えます。
動作するワードクラウド
ワードクラウドを描画するコードに行わせたいのは次のことです。
- RiTa.concordance()を使って、単語数のオブジェクト(counts)を構築する
- concordance()から返されたオブジェクトに対してキー/値のペア全部を繰り返し処理し、画面のランダムな位置に単語を描画する
- 単語の出現頻度(concordance()から返されたオブジェクトに含まれる値)にしたがって、テキストサイズを変更する
次のスケッチはこの例です。
let lines;
let counts;
function preload() {
lines = loadStrings('genesis.txt');
}
function setup() {
createCanvas(400, 300);
counts = RiTa.concordance(lines.join(" "));
background(50);
textAlign(CENTER, CENTER);
textSize(24);
noStroke();
fill(255);
noLoop();
}
function draw() {
for (var k in counts) {
if (counts.hasOwnProperty(k)) {
fill(random(255));
textSize(counts[k]);
text(k, random(width), random(height));
}
}
}
新しい箇所はdraw()ないのループです。このループは、countsオブジェクトのキー/値のペアごとにtextSize()とtext()関数を実行します。ループ内のkは現在の単語に評価され、counts[k]はその単語の出現回数に評価されます。
ストップワード
ほとんどの人は、上記のワードクラウドを見て、何か違うと言うでしょう。“and”や“the”は通常、重要な単語には含まれないと思われるからです。テキスト分析アプリケーションでは通常、こういったよく使用される小さな単語は、さまざまなテキストで同じような分布で幅広く現れ、実際には”何も意味していない”という想定によって、除外されます(しかし反対意見もあります)。
こうした小さな単語は”ストップワード”と呼ばれ、RiTaJSはそれを自動的に除外する機能を持っています。これを有効にするには、concordance()関数にパラメータを渡します。concordance()関数のドキュメンテーションを調べてみましょう。すると、ストップワードがコンコーダンス分析に含まれないようにするには、2つめのパラメータとして、ignoreStopWords(キー)をtrue(値)にしたオブジェクトを渡せばよいことが分かります。では次のサンプルで、大文字小文字の区別と句読点を無視するパラメータも含めて試してみましょう。
var params = {
ignoreStopWords: true, // ストップワードを無視
ignoreCase: true, // 大文字小文字の区別を無視
ignorePunctuation: true // 句読点を無視
};
let lines;
let counts;
function preload() {
lines = loadStrings('genesis.txt');
}
function setup() {
createCanvas(400, 300);
var params = {
ignoreStopWords: true, // ストップワードを無視
ignoreCase: true, // 大文字小文字の区別を無視
ignorePunctuation: true // 句読点を無視
};
// paramsも渡す
counts = RiTa.concordance(lines.join(" "), params);
background(50);
textAlign(CENTER, CENTER);
textSize(24);
noStroke();
fill(255);
noLoop();
}
function draw() {
for (var k in counts) {
if (counts.hasOwnProperty(k)) {
fill(random(255));
textSize(counts[k]);
text(k, random(width), random(height));
}
}
}
パラメータを指定するために、オブジェクトを作成してそれを関数に渡すという手順はJavaScriptでよく用いられます。この先、みなさんがライブラリを使用するときにも何度も目にするでしょう。
単語のサイズを比率で決める
ここまで大きく進歩しました。ワードクラウドを表示し、ストップワードと句読点を除外しました。しかしコードにはまだ、単語の表示が小さい、という問題があります。無論、単語は大きく表示できますが、欽定訳聖書第1章のように短くないテキストを使ったらどうなるでしょう? たとえば次のスケッチは、「高慢と偏見」の全文を使ったサンプルです(読み込みに少し時間がかかります時間がかかります)。
var lines;
var counts;
function preload() {
lines = loadStrings('austen.txt');
}
function setup() {
createCanvas(400, 300);
var params = {
ignoreStopWords: true,
ignoreCase: true,
ignorePunctuation: true
};
counts = RiTa.concordance(lines.join(" "),
params);
// set drawing parameters
background(50);
textAlign(CENTER, CENTER);
textSize(24);
noStroke();
fill(255);
noLoop();
}
function draw() {
for (var k in counts) {
if (counts.hasOwnProperty(k)) {
fill(random(255));
textSize(counts[k]);
text(k, random(width), random(height));
}
}
}
これは明らかにダメです。一部の単語は頻繁に登場するので、大きすぎて読めなくなっています。より良い方法は、サイズを単語が登場する頻度の絶対数で決めるのではなく、比率を用いる方法です。そうすれば、最頻出の単語のサイズが制御でき、ほかの単語も頻度に合わせた比率でサイズを決めることができます。
各単語の比率を計算するには、まず単語の総数を知る必要があります。そのための関数には次のようなものが記述できるでしょう。
function totalValues(obj) {
let total = 0;
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
total += obj[k];
}
}
return total;
}
この関数は与えられたオブジェクトに含まれる全値の合計を返します。1つの単語の登場回数をこの合計で割ると、合計コンコーダンスのどれだけがその単語で構成されるかを表すパーセンテージが得られます。この数値を使用すると、単語のサイズを適切に決めることができます。
次のスケッチでは、この関数を使っています。
let lines;
let counts;
function preload() {
lines = loadStrings('austen.txt');
}
function setup() {
createCanvas(400, 300);
const params = {
ignoreStopWords: true,
ignoreCase: true,
ignorePunctuation: true
};
counts = RiTa.concordance(lines.join(" "), params);
total = totalValues(counts);
background(50);
textAlign(CENTER, CENTER);
textSize(24);
noStroke();
fill(255);
noLoop();
}
function draw() {
for (let k in counts) {
if (counts.hasOwnProperty(k)) {
if (counts[k] / total > 0.001) {
fill(random(255));
textSize((counts[k] / total) * 10000);
text(k, random(width), random(height));
}
}
}
}
function totalValues(obj) {
let total = 0;
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
total += obj[k];
}
}
return total;
}
textSize((counts[k]/total) * 10000)の行は事実上、テキストの1%を占める単語は100ピクセルの高さで描画する、という意味です(この例ではまた、ifステートメントで、テキストの0.1%に満たない単語は表示しない、という条件も付けています。この条件を調整すると、結果をもっと派手にすることも地味にすることもできます)。
品詞のタグを付ける
英語ではよく単語を、名詞や動詞、副詞などの品詞を持つものとして分析します。単語の品詞は、文章中のその単語の構文上の特性を示すものです。RiTaJSには、文字列を取ってテキスト内の各単語の品詞の配列を返す.getPosTags()関数があります(テキストの単語の品詞を特定する処理は、テキストを”タグ付け”すると呼ばれます)。次のスケッチは簡単な例で、ユーザーが入力したテキストに対応したタグを表示します。
let field;
let button;
function setup() {
createCanvas(400, 300);
field = createInput();
button = createButton("Tag, you're it!");
button.mousePressed(tagText);
background(50);
textSize(24);
fill(255);
noStroke();
}
function draw() {}
function tagText() {
background(50);
// getPosTags()はタグの配列を返す
const tags = RiTa.getPosTags(field.value());
const tagStr = tags.join(" ");
text(tagStr, 10, 10, width - 20, height - 20);
}
RiTaJSのデフォルトの品詞タグはPENN品詞タグです。このタグでは、たとえばJJが”adjective”(形容詞)を意味するので、混乱する恐れがあります(getPosTags()の2つめのパラメータにtrueを渡すことで、もっと単純なスキームの使用に切り替えることもできます)。
次のスケッチは、欽定訳聖書第1章を読み込んで名詞だけを抽出します。
let lines;
// 名詞
const nouns = [];
function preload() {
lines = loadStrings('genesis.txt');
}
function setup() {
createCanvas(400, 400);
const params = {
ignoreStopWords: true,
ignoreCase: true,
ignorePunctuation: true
};
counts = RiTa.concordance(lines.join(" "), params);
for (let k in counts) {
if (counts.hasOwnProperty(k)) {
const tags = RiTa.getPosTags(k);
if (tags[0] == 'nn') {
nouns.push(k);
}
}
}
noLoop();
}
function draw() {
background(50);
textSize(24);
fill(255);
noStroke();
text(nouns.join(' '), 10, 10, width - 20, height - 20);
}
演習
getPosTags()を使って、与えられたテキストから形容詞か名詞または動詞だけを表示する、上記ワードクラウドスケッチの改良版を作成します。
RiLexicon
RiTaJSには、単語を取り出してそれに関する情報を見つけ出すためのさまざまな興味深い関数を持つ、RiLexiconという特別なオブジェクトがあります。
RiLexiconオブジェクトを作成するには、次のコードを記述します。
const lexicon = new RiLexicon();
すると、作成したlexiconオブジェクトから、以下に示すメソッドを呼び出すことができます。
ランダムな単語を取得する
RiLexiconオブジェクトの.randomWord()メソッドは、単語のリストから、ランダムな単語を返します。特定の条件を指定することもできます。次のスケッチは、画面のクリックでランダムな単語を表示する簡単なサンプルです。
let lexicon;
function setup() {
createCanvas(400, 400);
lexicon = new RiLexicon();
background(50);
fill(255);
noStroke();
textSize(24);
textAlign(CENTER, CENTER);
text("Click for word", width / 2, height / 2);
}
function draw() {}
function mousePressed() {
background(50);
text(lexicon.randomWord(), width / 2, height / 2);
}
パラメータを与えると、このメソッドは特定の品詞にのみマッチする単語を返します(Pennタグを使って)。次のスケッチは、単純なMad Libs風テキストを描画します。
なお、英語ではMad Libs感が伝わりづらいので、Google Apps Scriptを使って日本語に翻訳しています。
let lexicon;
function setup() {
createCanvas(400, 400);
lexicon = new RiLexicon();
background(50);
fill(255);
noStroke();
textSize(32);
textAlign(CENTER, CENTER);
text("Click for fun", width / 2, height / 2);
}
function draw() {
}
function mousePressed() {
background(50);
textAlign(LEFT, TOP);
//4月は〇〇な〇〇で、〇〇な〇〇から、〇〇を〇〇する
var output = "April is the " +
lexicon.randomWord("jj") + " " + // 形容詞
lexicon.randomWord("nn") + ", " + // 名詞(単数形)
lexicon.randomWord("vbg") + " " + // 動名詞か現在分詞
lexicon.randomWord("nns") + // 名詞(複数形)
" out of the " +
lexicon.randomWord("jj") + " " + // 形容詞
lexicon.randomWord("nn"); // 名詞(単数形)
text(output, 10, 10, width - 20, height - 20);
// 英語から日本語への翻訳 Google Apps Scriptを使用
const url = 'https://script.google.com/macros/s/AKfycbyG49Xo9-47h973XSXXDhFYwsX_ZKFM1cT9jwu2x8tdYiuLvfo/exec?text=' + output;
// 英単語をGoogle Apps Scriptに送り、結果の日本語単語を受け取る
fetch(url).then((response) => {
return response.text();
}).then((text) => {
createP(text);
});
}
2つめのパラメータを使用すると、randomWord()は与えられた音節数の単語を返します。この機能を使うと、次のHAIKUジェネレータが作成できます。
let lexicon;
function setup() {
createCanvas(400, 400);
lexicon = new RiLexicon();
background(50);
fill(255);
noStroke();
textSize(24);
textAlign(CENTER, CENTER);
text("Click for haiku", width / 2, height / 2);
}
function draw() {}
function mousePressed() {
background(50);
var firstLine = "the " +
lexicon.randomWord("jj", 2) + " " +
lexicon.randomWord("nn", 2);
var secondLine = lexicon.randomWord("vbg", 2) +
" in the " +
lexicon.randomWord("jj", 2) + " " +
lexicon.randomWord("nn", 1);
var thirdLine = "I " +
lexicon.randomWord("vbd", 2) + " " +
lexicon.randomWord("rb", 2);
text(firstLine, width / 2, 150);
text(secondLine, width / 2, 200);
text(thirdLine, width / 2, 250);
}
音から単語を得る
RiLexiconオブジェクトはまた、.rhymes()メソッドも持っています。このメソッドは、与えられた単語と韻を踏む単語のリストを返します(「韻を踏む」とは、sendとblend、enoughとstuff、directionとconnectionのように、2つ以上の単語が同じ音で終わることを言います)。さらに.similarBySound()メソッドは、与えられた単語の音に似た単語を返します。これを使用すると、滑らかな響きのテキストが生成できます。
次のスケッチはこの2つのメソッドのデモです。
let field;
let button1;
let button2;
let lexicon;
function setup() {
createCanvas(400, 300);
lexicon = new RiLexicon();
field = createInput();
button1 = createButton("韻を踏む");
button1.mousePressed(getRhymes);
button2 = createButton("似た音");
button2.mousePressed(getSimilar);
background(50);
textSize(24);
fill(255);
noStroke();
}
function draw() {}
function getRhymes() {
background(50);
var rhymes = lexicon.rhymes(field.value());
var rhymesStr = rhymes.join(" ");
text(rhymesStr, 10, 10, width - 20, height - 20);
}
function getSimilar() {
background(50);
var similar = lexicon.similarBySound(field.value());
var similarStr = similar.join(" ");
text(similarStr, 10, 10, width - 20, height - 20);
}