『言語処理 100 本ノック』に PHP で挑む (問題 37 ~ 39)
『言語処理 100 本ノック』に PHP で挑戦しています。今回は第 4 章の残りの問題を解きます。
www.cl.ecei.tohoku.ac.jp
37. 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
<?php require_once __DIR__ . '/read_mecab_data.php'; require_once __DIR__ . '/term_frequency.php'; $morphs = read_mecab_data('neko.txt.mecab'); $frequencies = term_frequency($morphs); arsort($frequencies); $top10 = array_slice($frequencies, 0, 10, true); $labels = json_encode(array_keys($top10)); $data = json_encode(array_values($top10)); ?> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script> </head> <body> <div id="container" style="width: 800px; padding: 4px; border: solid 1px;"> <canvas id="canvas"></canvas> </div> <script> window.onload = function() { var context = document.getElementById("canvas").getContext("2d"); window.myChart = new Chart(context, { type: "bar", data: { labels: <?= $labels ?>, datasets: [{ data: <?= $data ?>, backgroundColor: "hsl(240, 100%, 75%)" }] }, options: { title: { display: true, text: "頻度上位 10 語" }, scales: { yAxes: [{ ticks: { beginAtZero: true } }] }, legend: { display: false } } }); }; </script> </body> </html>
Chart.js を利用して、ウェブブラウザで表示できるように実装しました。
Chart.js | Open source HTML5 Charts for your website
プログラムは HTML の中に PHP スクリプトが埋め込まれた形式になっています。先頭部分でグラフに表示するデータを生成して、script 要素内の JavaScript に書き出します。今では、このように HTML と PHP を混在させる書き方は推奨されないと思いますが、今回は気にしないことにします。
PHP スクリプトの実装内容は、これまでの問題と大きな違いはありません。単語の出現頻度を計算して降順に並べ替え、array_slice で先頭の 10 語を取得します。array_slice の最後の引数は、配列のキーをそのまま維持するオプションです。$top10 はキーが単語の表層形、値が出現頻度になっているので、それぞれを JSON に変換して $labels と $data に格納します。
HTML 側の実装は、Chart.js のサンプルスクリプトを参考にしました。 GitHub リポジトリの samples ディレクトリに、Chart.js で表示できるさまざまなグラフのサンプルがあります。
GitHub - chartjs/Chart.js: Simple HTML5 Charts using the <canvas> tag
プログラムを実行するには、以下のコマンドで PHP のビルトインサーバを起動します*1。特にオプションを指定しなければ、カレントディレクトリがドキュメントルートになります*2。
$ php -S localhost:8000
ブラウザからアクセスすると、次のグラフが表示されます。
38. ヒストグラム
単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.
<?php require_once __DIR__ . '/read_mecab_data.php'; require_once __DIR__ . '/term_frequency.php'; $morphs = read_mecab_data('neko.txt.mecab'); $frequencies = term_frequency($morphs); $bins = array_fill(0, 10, 0); foreach ($frequencies as $frequency) { ++$bins[intval($frequency / 1000)]; } $labels = json_encode(range(500, 9500, 1000)); $data = json_encode($bins); ?> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script> </head> <body> <div id="container" style="width: 800px; padding: 4px; border: solid 1px;"> <canvas id="canvas"></canvas> </div> <script> window.onload = function() { var context = document.getElementById("canvas").getContext("2d"); window.myChart = new Chart(context, { type: "bar", data: { labels: <?= $labels ?>, datasets: [{ data: <?= $data ?>, backgroundColor: "hsl(240, 100%, 75%)" }] }, options: { title: { display: true, text: "ヒストグラム" }, legend: { display: false } } }); }; </script> </body> </html>
問題 37 と同様に実装します。出現頻度が最も大きな単語は「の」で 9,194 回だということが分かっているので、1,000 刻みで 10 階級に分割しました。左端が 1 回から 1,000 回です。実行結果は以下のとおりで、ほぼすべての単語が出現頻度の低いところに集中していることが分かります。
念のため、プログラムを変更して出現頻度が 1 から 20 の範囲を表示させてみると、次のようになりました。
39. Zipfの法則
単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.
<?php require_once __DIR__ . '/read_mecab_data.php'; require_once __DIR__ . '/term_frequency.php'; $morphs = read_mecab_data('neko.txt.mecab'); $frequencies = term_frequency($morphs); arsort($frequencies); $data = json_encode(array_map( function ($n, $count) { return [ 'x' => $n, 'y' => $count ]; }, range(1, count($frequencies)), $frequencies )); ?> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script> </head> <body> <div id="container" style="width: 800px; padding: 4px; border: solid 1px;"> <canvas id="canvas"></canvas> </div> <script> window.onload = function() { var context = document.getElementById("canvas").getContext("2d"); window.myChart = new Chart(context, { type: "line", data: { datasets: [{ data: <?= $data ?>, fill: false, borderColor: "hsl(240, 100%, 75%)", pointRadius: 0 }] }, options: { title: { display: true, text: "Zipf の法則" }, legend: { display: false }, scales: { xAxes: [{ type: "logarithmic", position: "bottom", }], yAxes: [{ type: "logarithmic", }] } } }); }; </script> </body> </html>
Chart.js で散布図を表示するには、data の各要素をオブジェクトとして (x, y) をそれぞれプロパティとして設定するようです。先頭の PHP スクリプト部分で、そのようなデータ構造を作成しています。グラフの設定としては、pointRadius: 0 を指定してマーカーを非表示にし、軸には type: "logarithmic" を指定して両対数グラフにしています。
対数グラフにすると軸のラベルが指数形式になるようです。また、X 軸を見ると、ラベルが重ならずに表示できる間隔に適当に間引かれています。このような挙動もオプションの設定で変更できます。たとえば scales の設定を次のように変更したところ、末尾のグラフが得られました。
scales: { xAxes: [{ type: "logarithmic", position: "bottom", ticks: { maxRotation: 90, minRotation: 90, callback: function($value) { return "125".indexOf(($value + "").substr(0, 1)) == -1 ? "" : $value; } } }], yAxes: [{ type: "logarithmic", ticks: { callback: function($value) { return "125".indexOf(($value + "").substr(0, 1)) == -1 ? "" : $value; } } }] }