最近、機械学習の話が増えています。ソーシャルメディアのフィードにはML、Python、TensorFlow、Spark、Scala、Goなどに関する記事が溢れています。その中で、PHPはどうかと思う人もいるのではないでしょうか。
PHPでの機械学習はどんな感じなのでしょうか。幸い、次のプロジェクトで利用できる汎用的な機械学習ライブラリーを実際に作った人がいます。本記事では、PHP用機械学習ライブラリー「PHP-ML」を紹介し、チャットやツイートのボットに利用できる感情分析クラスを作成します。具体的には以下の説明をします。
- 機械学習と感情分析の一般的な概念を説明する
- PHP-MLができることや、PHP_MLの短所を概説する
- 解決したい問題を定義する
- PHPで機械学習を用いることが、決して無謀なゴールではないことを証明する(オプション)
機械学習とは?
機械学習は人工知能のサブセットで、「明示的にプログラムしなくても学習できる能力」をコンピュータに与えることに焦点を当てています。特定のデータセットから「学習」できる、汎用的なアルゴリズムを用いて実現します。
機械学習の代表的な用途が「分類」です。分類アルゴリズムはデータを異なるグループやカテゴリーに分けるために用いられます。以下は分類アルゴリズムの例です。
- Eメールのスパムフィルター
- マーケットセグメンテーション
- 不正利用検知
機械学習は、さまざまなタスクで用いる多くの「一般的なアルゴリズム」を指す総称です。学習方法に応じて2つのアルゴリズムタイプに分類されます。「教師あり学習」と「教師なし学習」です。
教師あり学習
教師あり学習は、ラベル付きデータを入力オブジェクト(ベクトル)および望ましい出力値として使用し、アルゴリズムを訓練します。アルゴリズムは訓練データを解析し、予測関数と呼ばれる、ラベルの付いていないデータセットを新たに適用するための関数を生成します。
本記事は関係性の確認と検証が容易な教師あり学習に焦点を当てます。しかし両方のアルゴリズムとも等しく重要で、興味深いものです。ラベル付きデータが必要ないことから、教師なし学習のほうが便利であると考える人もいます。
教師なし学習
教師なし学習は、ラベルの付いていないデータを使用します。データセットの好ましい出力値は不明なので、アルゴリズムはデータセットから予測します。教師なし学習はデータに隠されたパターンを見つけるための探索的データ分析に向いています。
PHP-ML
PHP-MLはPHPで機械学習の斬新なアプローチを提供するライブラリーです。アルゴリズム、ニューラルネットワーク、そしてデータプリプロセッシングや交差検証、特徴抽出のためのツールが実装されています。
PHPの強みが機械学習アプリケーションに向いていないため、良く用いられる選択肢ではありません。しかし、すべての機械学習アプリケーションがペタバイト単位のデータを処理したり膨大な演算をしているわけではありません。単純なアプリケーションなら、PHPとPHP-MLでも可能です。
現時点で思いつく使用例は、スパムフィルターや感情分析などの分類器の実装です。分類問題を定義し、ソリューションを段階的に作成することで、PHP-MLをプロジェクトで使用する方法を解説します。
問題
PHP-MLを実装し、機械学習アプリケーションの作成手順を実演するために、楽しく取り組める問題を探しました。ツイートを感情分析するクラスを構築することで分類器を説明することにしました。
機械学習プロジェクトを成功させる鍵の1つは「最初に適切なデータセットを用意すること」です。データセットがなければ、分類済みサンプルによる分類器の訓練はできません。最近のメディアで、航空会社に対する抗議の声が多く見られることから、カスタマーから航空会社へのツイートをデータセットに使うことにします。
ツイートのデータセットはKaggle.ioのTwitter US Airline Sentimentからダウンロードできます。
ソリューション
処理する生のデータセットには以下の列があります。
- tweet_id
- airline_sentiment
- airline_sentiment_confidence
- negativereason
- negativereason_confidence
- airline
- airline_sentiment_gold
- name
- negativereason_gold
- retweet_count
- text
- tweet_coord
- tweet_created
- tweet_location
- user_timezone
以下に例を示します(横スクロールの表)。
tweet_id | airline_sentiment | airline_sentiment_confidence | negativereason | negativereason_confidence | airline | airline_sentiment_gold | name | negativereason_gold | retweet_count | text | tweet_coord | tweet_created | tweet_location | user_timezone |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
570306133677760513 | ニュートラル | 1.0 | ヴァージン・アメリカ | cairdin | 0 | @VirginAmerica What @dhepburn said. | 2015-02-24 11:35:52 -0800 | 東部標準時(米国&カナダ) | ||||||
570301130888122368 | ポジティブ | 0.3486 | 0.0 | ヴァージン・アメリカ | jnardino | 0 | @VirginAmerica plus you’ve added commercials to the experience… tacky. | 2015-02-24 11:15:59 -0800 | 太平洋標準時(米国&カナダ) | |||||
570301083672813571 | ニュートラル | 0.6837 | ヴァージン・アメリカ | yvonnalynn | 0 | @VirginAmerica I didn’t today… Must mean I need to take another trip! | 2015-02-24 11:15:48 -0800 | Lets Play | 中部標準時(米国&カナダ) | |||||
570301031407624196 | ネガティブ | 1.0 | ひどいフライト | 0.7033 | ヴァージン・アメリカ | jnardino | 0 | “@VirginAmerica it’s really aggressive to blast obnoxious “”entertainment”” in your guests’ faces & they have little recourse” | 2015-02-24 11:15:36 -0800 | 太平洋標準時(米国&カナダ) | ||||
570300817074462722 | ネガティブ | 1.0 | 非公開 | 1.0 | ヴァージン・アメリカ | jnardino | 0 | @VirginAmerica and it’s a really big bad thing about it | 2015-02-24 11:14:45 -0800 | 太平洋標準時(米国&カナダ) | ||||
570300767074181121 | ネガティブ | 1.0 | 非公開 | 0.6842 | ヴァージン・アメリカ | jnardino | 0 | “@VirginAmerica seriously would pay $30 a flight for seats that didn’t have this playing. | 2015-02-24 11:14:33 -0800 | 太平洋標準時(米国&カナダ) | ||||
570300616901320704 | ポジティブ | 0.6745 | 0.0 | ヴァージン・アメリカ | cjmcginnis | 0 | “@VirginAmerica yes | nearly every time I fly VX this “ear worm” won’t go away :)” | 2015-02-24 11:13:57 -0800 | サンフランシスコ、カリフォルニア | 太平洋標準時(米国&カナダ) | |||
570300248553349120 | ニュートラル | 0.634 | ヴァージン・アメリカ | pilot | 0 | “@VirginAmerica Really missed a prime opportunity for Men Without Hats parody | there. https://t.co/mWpG7grEZP” | 2015-02-24 11:12:29 -0800 | ロサンゼルス | 太平洋標準時(米国&カナダ) |
ファイルには14,640ツイート格納されています。使用するデータセットとしては十分です。列の数も多く、本記事で必要とする以上のデータがそろっています。以下の列を使用します。
- text
- airline_sentiment
textが「特徴量」で、airline_sentimentが「目標値」です。残りの列は使いません。プロジェクトを作成し、以下のファイルを使い、コンポーザーを初期化します。
{
"name": "amacgregor/phpml-exercise",
"description": "Example implementation of a Tweet sentiment analysis with PHP-ML",
"type": "project",
"require": {
"php-ai/php-ml": "^0.4.1"
},
"license": "Apache License 2.0",
"authors": [
{
"name": "Allan MacGregor",
"email": "amacgregor@allanmacgregor.com"
}
],
"autoload": {
"psr-4": {"PhpmlExercise\\": "src/"}
},
"minimum-stability": "dev"
}
composer install
コンポーザーのガイドは、こちらを参照ください。
Tweets.csvデータファイルを読み込む簡単なスクリプトを作成して必要なデータが格納できていればセットアップは成功です。以下のコードをコピーしてプロジェクトのルートにはります。名前はreviewDataset.phpとします。
<?php
namespace PhpmlExercise;
require __DIR__ . '/vendor/autoload.php';
use Phpml\Dataset\CsvDataset;
$dataset = new CsvDataset('datasets/raw/Tweets.csv',1);
foreach ($dataset->getSamples() as $sample) {
print_r($sample);
}
php reviewDataset.phpでスクリプトを実行し、出力を確認します。
Array( [0] => 569587371693355008 )
Array( [0] => 569587242672398336 )
Array( [0] => 569587188687634433 )
Array( [0] => 569587140490866689 )
役に立つとは思えません。内部で起きていることをCsvDatasetクラスで確認します。
<?php
public function __construct(string $filepath, int $features, bool $headingRow = true)
{
if (!file_exists($filepath)) {
throw FileException::missingFile(basename($filepath));
}
if (false === $handle = fopen($filepath, 'rb')) {
throw FileException::cantOpenFile(basename($filepath));
}
if ($headingRow) {
$data = fgetcsv($handle, 1000, ',');
$this->columnNames = array_slice($data, 0, $features);
} else {
$this->columnNames = range(0, $features - 1);
}
while (($data = fgetcsv($handle, 1000, ',')) !== false) {
$this->samples[] = array_slice($data, 0, $features);
$this->targets[] = $data[$features];
}
fclose($handle);
}
CsvDatasetのコンストラクターは3つの引数を取ります。
- ソースCSVへのファイルパス
- ファイル中の特徴量の数を指定する正数
- 先頭行がヘッダーかを示すブーリアン
クラスを詳しく見ると、CSVファイルを2つの内部配列samplesとtargesにマッピングしていると分かります。samplesはファイルのすべての特徴量を格納して、targesは既知の値(ネガティブ、ポジティブ、ニュートラル)を格納します。
CSVファイルのフォーマットは以下だと分かります。
| feature_1 | feature_2 | feature_n | target |
作業を進めるために、必要な列のみのクリーンなデータセットを生成するためのスクリプトgenerateCleanDataset.phpを作成します。
<?php
namespace PhpmlExercise;
require __DIR__ . '/vendor/autoload.php';
use Phpml\Exception\FileException;
$sourceFilepath = __DIR__ . '/datasets/raw/Tweets.csv';
$destinationFilepath = __DIR__ . '/datasets/clean_tweets.csv';
$rows =[];
$rows = getRows($sourceFilepath, $rows);
writeRows($destinationFilepath, $rows);
/**
* @param $filepath
* @param $rows
* @return array
*/
function getRows($filepath, $rows)
{
$handle = checkFilePermissions($filepath);
while (($data = fgetcsv($handle, 1000, ',')) !== false) {
$rows[] = [$data[10], $data[1]];
}
fclose($handle);
return $rows;
}
/**
* @param $filepath
* @param string $mode
* @return bool|resource
* @throws FileException
*/
function checkFilePermissions($filepath, $mode = 'rb')
{
if (!file_exists($filepath)) {
throw FileException::missingFile(basename($filepath));
}
if (false === $handle = fopen($filepath, $mode)) {
throw FileException::cantOpenFile(basename($filepath));
}
return $handle;
}
/**
* @param $filepath
* @param $rows
* @internal param $list
*/
function writeRows($filepath, $rows)
{
$handle = checkFilePermissions($filepath, 'wb');
foreach ($rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
}
必要最低限の機能だけを持ったコードで、そこまで複雑ではありません。phpgenerateCleanDataset.phpで実行します。
reviewDataset.phpスクリプトでクリーンになったデータセットを読み込みます。
Array
(
[0] => @AmericanAir That will be the third time I have been called by 800-433-7300 an hung on before anyone speaks. What do I do now???
)
Array
(
[0] => @AmericanAir How clueless is AA. Been waiting to hear for 2.5 weeks about a refund from a Cancelled Flightled flight & been on hold now for 1hr 49min
)
うまく行きました。使えるデータが格納されています。ここまではデータを操作する単純なスクリプトの作成でした。次は、src/classification/SentimentAnalysis.phpに新たなクラスを作成します。
<?php
namespace PhpmlExercise\Classification;
/**
* Class SentimentAnalysis
* @package PhpmlExercise\Classification
*/
class SentimentAnalysis {
public function train() {}
public function predict() {}
}
感情分析クラスに2つの関数を実装します。
- 訓練関数:訓練サンプルとラベルのデータセット、およびいくつかの最適パラメータを受け取る
- 予測関数:ラベル付けされていないデータセットを受け取り、訓練データに基づき一連のラベルを割り当てる
プロジェクトのルートにスクリプト「classifyTweets.php」を作成します。感情分析クラスのインスタンスを生成し、テストを実施します。以下が使用するテンプレートです。
<?php
namespace PhpmlExercise;
use PhpmlExercise\Classification\SentimentAnalysis;
require __DIR__ . '/vendor/autoload.php';
// ステップ1:データセットの読み込み
// ステップ2:データセットの準備
// ステップ3:訓練用データセットの生成
// ステップ4:分類器の訓練
// ステップ5:分類器の正確性をテストする
ステップ1:データセットの読み込み
すでにあるCSVをdatasetオブジェクトに読み込む基本コードを微調整して使います。
<?php
...
use Phpml\Dataset\CsvDataset;
...
$dataset = new CsvDataset('datasets/clean_tweets.csv',1);
$samples = [];
foreach ($dataset->getSamples() as $sample) {
$samples[] = $sample[0];
}
1つの特徴量(今回の場合、ツイート文)だけを格納した一次元配列が生成されます。特徴量を用いて分類器を訓練します。
ステップ2:データセットの準備
生のテキストを分類器に渡しても、ツイートはそれぞれ本質的に異なるため利用できず、また正確性もありません。しかし、幸いなことにテキストを処理して分類や機械学習アルゴリズムに適用できるようにするための方法がいくつかあります。この記事では、以下の2つのクラスを使います。
- Token Count Vectorizer(トークン数ベクトライザー):テキストサンプルのコレクションをトークン数のベクトルに変換する。すなわち、ツイートの各単語を重複しない数値に変えて、あるテキストサンプルにおける単語の出現回数を監視する
- Tf-idf Transformer(Tf-idfトランスフォーマー):term frequency(単語の出現頻度)- inverse document frequency(逆文書頻度)の略。コレクションまたはコーパス内のドキュメントにとって、ある単語がどれだけ重要かを示すための統計値
テキストのベクトライザーを使います。
<?php
...
use Phpml\FeatureExtraction\TokenCountVectorizer;
use Phpml\Tokenization\WordTokenizer;
...
$vectorizer = new TokenCountVectorizer(new WordTokenizer());
$vectorizer->fit($samples);
$vectorizer->transform($samples);
Tf-idfトランスフォーマーを適用します。
<?php
...
use Phpml\FeatureExtraction\TfIdfTransformer;
...
$tfIdfTransformer = new TfIdfTransformer();
$tfIdfTransformer->fit($samples);
$tfIdfTransformer->transform($samples);
サンプル配列が分類器を簡単に理解できるフォーマットになりました。続いて、各サンプルを対応する感情でラベル付けします。
ステップ3:訓練用データセットの生成
PHP-MLにはこのニーズに対応するための機能が用意されていて利用するコードはとても簡単です。
<?php
...
use Phpml\Dataset\ArrayDataset;
...
$dataset = new ArrayDataset($samples, $dataset->getTargets());
データセットを使って分類器を訓練します。検証に使うテストデータセットがないため、元のデータセットを半分に分けるちょっとした「チート」を使います。片方は訓練用データセット、もう片方ははるかに少ない量の、モデルの正確性をテストするために使うデータセットです。
<?php
...
use Phpml\CrossValidation\StratifiedRandomSplit;
...
$randomSplit = new StratifiedRandomSplit($dataset, 0.1);
$trainingSamples = $randomSplit->getTrainSamples();
$trainingLabels = $randomSplit->getTrainLabels();
$testSamples = $randomSplit->getTestSamples();
$testLabels = $randomSplit->getTestLabels();
この手法は交差検証と呼ばれています。この用語は統計学から来ていて、定義は以下のとおりです。
交差検証(交差確認)(こうさけんしょう、英: Cross-validation)とは、統計学において標本データを分割し、その一部をまず解析して、残る部分でその解析のテストを行い、解析自身の妥当性の検証・確認に当てる手法を指す。データの解析(および導出された推定・統計的予測)がどれだけ本当に母集団に対処できるかを良い近似で検証・確認するための手法である。
Wikipedia
ステップ4:分類器の訓練
SentimentAnalysisクラスを実装します。機械学習の大半はデータを収集し加工することです。機械学習メソッドを実装する比率はわずかです。
感情分析クラスの実装に当たって、3つの分類アルゴリズムを使用できます。
- Support Vector Classification(サポートベクター分類)
- KNearestNeighbors(K近傍法)
- NaiveBayes(単純ベイズ)
今回は上記の中でもっとも単純な単純ベイズ分類器を使います。クラスを更新して訓練メソッドを実装します。
<?php
namespace PhpmlExercise\Classification;
use Phpml\Classification\NaiveBayes;
class SentimentAnalysis
{
protected $classifier;
public function __construct()
{
$this->classifier = new NaiveBayes();
}
public function train($samples, $labels)
{
$this->classifier->train($samples, $labels);
}
}
面倒な作業はすべてPHP-MLに任せて、作るのはプロジェクトのための少しの抽象的な処理だけです。分類器が実際に訓練を実施し、うまく動いていることを確認するには、testSamplesとtestLabelsを使います。
ステップ5:分類器の正確性をテストする
分類器をテストする前に、予測メソッドを実装します。
<?php
...
class SentimentAnalysis
{
...
public function predict($samples)
{
return $this->classifier->predict($samples);
}
}
ここでも、PHP-MLが面倒な作業を実施します。classifyTweetsクラスも合わせて更新します。
<?php
...
$predictedLabels = $classifier->predict($testSamples);
訓練したモデルの正確性をテストします。PHP-MLが用意している方法を使います。PHP-MLには複数のmetric(指標)クラスがありますが、今回はモデルの正確性を測るのが目的です。
<?php
...
use Phpml\Metric\Accuracy;
...
echo 'Accuracy: '.Accuracy::score($testLabels, $predictedLabels);
以下の行が表示されます。
Accuracy: 0.73651877133106%
最後に
本記事で学んだことを要約します。
- 最初に良いデータセットを用意することが機械学習アルゴリズムを実装する上で鍵となる
- 教師あり学習と教師なし学習の違い
- 交差検証の意味と機械学習における利用
- ベクトル化と変換が機械学習のテキストデータセットを準備する上で必要不可欠
- PHP-MLの単純ベイズ分析器を使ったTwitter感情分析の実装方法
PHP-MLライブラリーを紹介しました。ライブラリーができることや、プロジェクトに組み込む方法についてうまく説明できていれば幸いです。
この記事だけではなく、学ぶべきことや、改善、実験すべきことは多くあります。機能を改善するためのアイディアです。
- 単純ベイズアルゴリズムをサポートベクトル分類アルゴリズムに変える
- 全データセット(14,000行)を用いて機械学習を実行すると、プロセスがメモリを大量に使用するはず。モデルに永続性を与え、実行のたびに訓練する必要がないようにする
- データセット生成をヘルパークラス内に移動する
本記事はWern Anchetaが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:How to Analyze Tweet Sentiments with PHP Machine Learning)
[翻訳:薮田佳佑/編集:Livit]