PHPを使って形態素解析と文章の類似度を出してみる

ご無沙汰しております。
taanatsuです。

今回は珍しくPHPの記事を書いていこうと思います。
ExciteといえばPHPですからね!しらんけど。

形態素解析

皆さんは「形態素解析」という言葉を耳にしたことがありますでしょうか?
機械学習だ!AIだ!と騒がれる昨今、文章の解析で使われる手法の一つがこの形態素解析です。
私は漢字が4つ以上並ぶと読めなくなるので、形態素解析という言葉が苦手ではあります。

形態素解析とは、文章を「形態素」、いわゆる名詞・動詞・形容詞・副詞のような、日本語の最小単位の単語に分割する処理のことを言います。

形態素解析器「MeCab

形態素解析を行ってくれるツールです。
今回はよく使われる「MeCab」を利用していきたいと思います。

で、Windowsの方はすいません。。。
会社のPCがMacなので、この記事はMac用になります。
私個人はWindows機を利用していて、できることは知っていますので、Windowsの方は頑張ってください!
(確かバイナリをダウンロードして、ダブルクリックしてインストールするだけだったはず…!)

MeCabのインストール

Homebrewを使えば一発です。

$ brew install mecab

以上!
……と言いたいところですが、文章を単語に分割するための「辞書」が必要になります。
辞書も入れましょう。

$ brew install mecab-ipadic

以上です。
ターミナル上でmecabとタイプしてEnterを押してみてください。
入力待ちになります。

この状態で「すもももももももものうち」と入力してEnterを押してみましょう。

$ mecab            
すもももももももものうち
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

きれいに分割されていますね!
これが形態素解析です。

PHPからMeCabを使う

さて、こういう分野ではPythonブイブイ言わせていますが、弊社はPHPが盛んな会社ですので、PHPで使っていきましょう。 PHP7.1くらいまでは php-mecabというPHPMeCabを使えるようにするバインディングツールがあったのですが、どうやらPHP8には対応していなさそう…
なので、ちょっと強引ですが、PHPexecを使っていきます。
参考

<?php
$result = [];

$text = 'すもももももももものうち';
exec('echo ' . $text . ' | mecab', $result);

var_dump($result);

array(8) {
  [0] =>
  string(61) "すもも    名詞,一般,*,*,*,*,すもも,スモモ,スモモ"
  [1] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [2] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [3] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [4] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [5] =>
  string(40) "の        助詞,連体化,*,*,*,*,の,ノ,ノ"
  [6] =>
  string(63) "うち      名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ"
  [7] =>
  string(3) "EOS"
}

形態素解析を使った文章の類似度

それでは形態素解析を使って文章の類似度を出してみましょう。
2つの文章に登場した形態素に対して、文章がその形態素を持っていれば1、持っていなければ0として計算してみましょう。

<?php
$text1 = '新宿は豪雨';
$text2 = '渋谷は豪雨';

echo cosineSimilarity($text1, $text2);

/**
 * 分かち書きのリストを作成します
 * 
 * @param string $text 分かち書きを作成したい文章
 */
function getWakachiList(string $text): array {
    $result = '';
    exec('echo ' . $text . ' | mecab -Owakati', $result);

    if (!is_array($result) || count($result) !== 1) {
        return [];
    }

    return explode(' ', $result[0]);
}


/**
 * 文章の類似度をコサイン類似度を用いて求めます
 * 
 * @param string $text1 文章1つ目
 * @param string $text2 文章2つ目
 */
function cosineSimilarity(string $text1, string $text2): float {
    // 文章を形態素に分解
    $text1Corpus = getWakachiList($text1);
    $text2Corpus = getWakachiList($text2);

    // 2つの文章の形態素を抽出
    $allCorpus = array_unique(array_merge($text1Corpus, $text2Corpus));

    // コサイン類似度の計算に必要な分子分母の変数
    $c = 0;
    $m1 = 0;
    $m2 = 0;

    foreach ($allCorpus as $word) {
        // 文章1に対象の形態素があるかどうか(あれば1、なければ0)
        $n1 = (array_search($word, $text1Corpus) !== false) ? 1 : 0;
        // 文章2に対象の形態素があるかどうか(あれば1、なければ0)
        $n2 = (array_search($word, $text2Corpus) !== false) ? 1 : 0;

        // コサイン類似度に利用する分子分母の数値を計算
        $c += ($n1 * $n2);
        $m1 +=  $n1 * $n1;
        $m2 += $n2 * $n2;
    }

    // コサイン類似度の計算
    if ($m1 === 0 || $m2 === 0) {
        return 0;
    }

    return $c / (sqrt($m1) * sqrt($m2));
}

新宿は豪雨
渋谷は豪雨

以上のテキストから形態素を抽出します。
また各文章にその形態素があるかないかも確認します。

新宿 豪雨 渋谷
テキスト1 1(形態素を持っている) 1 1 0(形態素を持っていない)
テキスト2 0 1 1 1

上記の1と0を使って、コサイン類似度を算出します。

このように形態素解析を使うと、単語単位で分解できるため、精度良く文章の類似度を出すことができます。
それでは今回はこのあたりで!