y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

『言語処理 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

ブラウザからアクセスすると、次のグラフが表示されます。
f:id:y_uti:20160820060612p:plain

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 回です。実行結果は以下のとおりで、ほぼすべての単語が出現頻度の低いところに集中していることが分かります。
f:id:y_uti:20160820060846p:plain

念のため、プログラムを変更して出現頻度が 1 から 20 の範囲を表示させてみると、次のようになりました。
f:id:y_uti:20160820060854p:plain

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" を指定して両対数グラフにしています。
f:id:y_uti:20160820060904p:plain

対数グラフにすると軸のラベルが指数形式になるようです。また、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; }
    }
  }]
}

f:id:y_uti:20160820091550p:plain

*1:もちろん、ビルトインサーバ以外の方法でも確認できます。たとえばウェブサーバを利用しない方法としては、作成したスクリプトphp コマンドで実行すれば HTML ファイルが出力されるので、それを開けば実行結果を確認できます。

*2:-t オプションでドキュメントルートを変更できます。