PHPで機械学習(その1):ツイートを取得しDB格納

(1/1)
人工知能(AI)に再び脚光が当たっている。とくに、ディープラーニングをはじめとする機械学習が話題だ。
機械学習には、あらかじめ与える学習データの質と量が大切だが、ネット時代になり、家にいながらにして学習データが集められるようになった。
そこで今回は、PHPを使って機械学習の基本について紹介することにしよう。

機械学習は、大まかに分けて3つの段取りを踏む。
  1. 学習データの収集(ストア)
  2. 収集したデータを学習(トレーニング)
  3. 学習結果を使って新たなデータを判定(テスト)
今まで紹介してきたプログラミング技術を総動員するわけだが、コード量が多いので、節を分けて紹介していく。

学習データは、複数ユーザのツイートを使うことにする。
今回は、まず、複数ユーザのツイートを学習デートして収集し、データベースに格納するところまでを紹介する。

(2021年7月11日)PHP8対応,リファラ・チェック改良

目次

サンプル・プログラムの実行例

PHPで機械学習(その1):ツイートを取得しDB格納

サンプル・プログラム

圧縮ファイルの内容
BayesClassifier.phpサンプル・プログラム
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。

サンプル・プログラムの流れ

PHPで機械学習(その1):ツイートを取得しDB格納
主要処理はユーザー定義クラス pahooLearningTweets に記述している。
コンストラクタでは、TwitterAPI の利用と、データベース SQLite の準備を行う。

本プログラムに $screen_name が渡された場合は、TwitterPI を使って対応するユーザーIDを取得。もしDB未登録であれば、これを登録する。
続いて、DBから、そのユーザーの最小・最大ツイートIDを取得する。これは、次に TwitterPI を使って複数ツイートを取得する際に、取得基準となるIDを指定するための作業である。もしDB登録がなければ、最新ツイートを取得する。取得したツイートは、DBに登録する。

自動処理かどうかを表すフラグ $auto が立っていなければ、登録されている全ユーザー情報を取得し、画面に表示する。
このとき、前回 TwitterPI コールからの経過時間をチェックし、一定時間が経過していなければアクションが起こせないようにしておく。これは、大量の呼び出しを受け付けないという TwitterPI の仕様があるためだ。

自動処理は、LinuxのcronまたはWindowsのタスクスケジューラを使い、バックグラウンドで学習データを収集するための仕組みとして用意した。詳細は、本節の最後に述べる。

準備

0040: //出力ログ・レベル
0041: define('LOG_LEVEL', 1); //0:エラーのみ,1:最小限の成功ログまで,2:全部
0042: 
0043: //一度に取得するツイート数
0044: define('NUM_TWEETS', 80);
0045: 
0046: //一度に学習するツイート数
0047: define('NUM_LEARN', 100);
0048: 
0049: //TwitterAPI呼び出し間隔(秒)
0050: define('TIME_INTERVAL', (10 * 60));
0051: 
0052: //TwitterAPI呼び出しパラメタ(デバッグ用)
0053: define('TWITTERAPI_VAR', 1);
0054: 
0055: //SQLite DBファイル名;各自の環境に合わせて変更すること
0056: define('DBFILE', './sqlite/usertimelines.sqlite3');
0057: 
0058: //MeCab実行プログラム;各自の環境に合わせて変更すること
0059: define('MECAB', 'C:\Program Files (x86)\MeCab\bin\mecab.exe');
0060: 
0061: //ユーザー辞書;各自の環境に合わせて変更すること
0062: define('FILE_UDIC_MECAB', 'C:\Program Files (x86)\MeCab\dic\user.dic');

本プログラムでは、データベースや外部APIを使うことなどから、予想外のトラブルが発生することが予想される。そこで、アプリケーションログをファイルに書き出す関数 putAppLog を用意した。出力ログ・レベルは定数 LOG_LEVEL に定義する。

あるユーザーのツイートを同時に複数取得するが、その最大数を決めるのが NUM_TWEETS である。TwitterPI の仕様上、最大200であるが、負荷をかけないよう少なめの数字としておこう。
また、前述のように、大量の呼び出しを受け付けないという TwitterPI の仕様を受け、呼び出し間隔を TIME_INTERVA で定義しておく。

収集した学習データはデータベース SQLite に格納する。
SQLite についは「PHPとデータベース」で紹介しているが、PHP5以上に組み込まれているものであり、とくに追加作業することなく使える。
データベースは単一ファイルに格納され、ファイル名を DBFILE に定義しておく。

Twitter APIを利用する関数群はユーザー定義クラス pahooTwitterAPI として別ファイル "pahooTwitterAPI.php" に定義してある。このファイルを  require_once  できるパスに配置すること。このクラスの内容については「PHPでTwitterに投稿(ツイート)する」などを参照いただきたい。

pahooLearningTweets クラス

0135: // pahooLearningTweetsクラス =================================================
0136: class pahooLearningTweets {
0137:     var $ptw;                            //pahooTwitterAPIクラス
0138:     var $pdo;                            //DBアクセス
0139:     var $error;                      //エラーフラグ
0140:     var $errmsg;                     //エラーメッセージ
0141:     var $table_logAPI  = 'log_api'; //テーブル:TwitterAPI利用ログ
0142:     var $table_users   = 'users';       //テーブル:ユーザー
0143:     var $table_tweets  = 'tweets';      //テーブル:ツイート
0144:     var $table_vectors = 'vectors'; //テーブル:学習記録
0145:     var $fname_mylog   = './log/';      //アプリケーション・ログファイル
0146:     var $level_mylog   = LOG_LEVEL;  //ログ出力レベル
0147: 
0148: /**
0149:  * コンストラクタ
0150:  * @param   なし
0151:  * @return  なし
0152: */
0153: function __construct() {
0154:     $this->error  = FALSE;
0155:     $this->errmsg = '';
0156: 
0157:     //アプリケーション・ログファイルの準備=プログラムファイル名.log
0158:     $arr = pathinfo($_SERVER['SCRIPT_NAME']);
0159:     $this->fname_applog = $this->fname_mylog . $arr['filename'] . '.log';
0160: 
0161:     //pahooTwitterAPIクラス
0162:     $this->ptw = new pahooTwitterAPI(TWITTERAPI_VAR);
0163: 
0164:     //SQLite準備
0165:     $this->pdo = NULL;
0166:     try {
0167:         $this->pdo = new PDO('sqlite:' . DBFILE);
0168:         $this->pdo->setAttribute(PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION);
0169: 
0170:         //テーブル作成:TwitterAPI利用ログ
0171:         //user_id:ログID, dt:利用日時, count:取得ツイート数
0172:         $this->pdo->exec('CREATE TABLE IF NOT EXISTS ' . $this->table_logAPI . '(
0173:             id       INTEGER PRIMARY KEY AUTOINCREMENT,
0174:             url      TEXT,
0175:             count    INTEGER,
0176:             dt       TEXT
0177:         )
');
0178: 
0179:         //テーブル作成:ユーザー情報
0180:         //user_id:ユーザーID, screen_name:スクリーンネーム,name:名前
0181:         //active:アクティブ・フラグ, dt:登録日時
0182:         $this->pdo->exec('CREATE TABLE IF NOT EXISTS ' . $this->table_users . '(
0183:             user_id     TEXT PRIMARY KEY,
0184:             screen_name TEXT,
0185:             name        TEXT,
0186:             active      INTEGER(1) NOT NULL DEFAULT 1,
0187:             dt          TEXT
0188:         )
');
0189: 
0190:         //テーブル作成:ツイート内容
0191:         //id:メッセージID, user_id:ユーザーID, description:ツイート内容,
0192:         //flag:学習済みフラグ, dt:登録日時
0193:         $this->pdo->exec('CREATE TABLE IF NOT EXISTS ' . $this->table_tweets . '(
0194:             id          TEXT PRIMARY KEY,
0195:             user_id     TEXT,
0196:             description TEXT,
0197:             flag        INTEGER,
0198:             dt          TEXT
0199:         )
');
0200: 
0201:         //テーブル作成:学習記録
0202:         //id:学習ID, user_id:ユーザーID, word:単語, count:出現回数
0203:         //dt:登録日時
0204:         $this->pdo->exec('CREATE TABLE IF NOT EXISTS ' . $this->table_vectors . '(
0205:             id          TEXT PRIMARY KEY,
0206:             user_id     TEXT,
0207:             word        TEXT,
0208:             count       INTEGER,
0209:             dt          TEXT
0210:         )
');
0211: 
0212:     } catch (PDOException $e) {
0213:         $this->error  = TRUE;
0214:         $this->errmsg = 'Error SQLite: ' . $e->getMessage();
0215:         $this->putAppLog($this->errmsg__LINE____FUNCTION__);
0216:     }
0217: }

次節以降でプログラムを流用することを考え、主要処理はユーザー定義クラス pahooLearningTweets に記述している。

コンストラでは、前述のオブジェクト pahooTwitterAPI を生成する。
続いてデータベース SQLite の準備を行う。データベースがなければ、自動的に生成する。

テーブル定義

$table_users:ユーザー情報
No. 名前 内容
1user_idテキストユーザーID
2screen_nameテキストスクリーンネーム
3dtテキスト登録日時
$table_tweets:ツイート内容
No. 名前 内容
1idテキストツイートID
2user_idテキストユーザーID
3descriptionテキストツイート内容
4flag整数学習済みフラグ
5dtテキスト登録日時
$table_vectors:学習記録
No. 名前 内容
1idテキスト管理ID
2user_idテキストユーザーID
3wordテキスト単語
4count整数出現回数
5dtテキスト登録/更新日時
$table_logAPI:TwitterAPI利用ログ
No. 名前 内容
1id整数管理ID
2urlテキストTwitterAPI URL
3count整数取得ツイート数
4dtテキスト利用日時

アプリケーション・ログ

0245: /**
0246:  * アプリケーション・ログに書き込む
0247:  * @param   string $msg  メッセージ
0248:  * @param   int    $level出力レベル(0:再優先~)
0249:  * @param   int    $line 行番号
0250:  * @param   string $func 関数名
0251:  * @return  bool TRUE/FALSE
0252: */
0253: function putAppLog($msg$level$line$func) {
0254:     //出力レベルのチェック
0255:     if ($level > $this->level_mylogreturn TRUE;
0256: 
0257:     //タイムスタンプ
0258:     $dt = date(DATE_W3Ctime());
0259:     //パスが無ければ生成
0260:     $arr = pathinfo($this->fname_applog);
0261:     if (! file_exists($arr['dirname'])) {
0262:         mkdir($arr['dirname']);
0263:     }
0264:     //ログファイルが無ければ生成
0265:     if (! file_exists($this->fname_applog)) {
0266:         $outfp = fopen($this->fname_applog, 'w');
0267:         if ($outfp == FALSE)    return FALSE;
0268:         fwrite($outfp$dt . " -- make new log file.\n");
0269:         fclose($outfp);
0270:     }
0271:     //ログ書き込み
0272:     $outfp = fopen($this->fname_applog, 'a');
0273:     if ($outfp == FALSE)    return FALSE;
0274:     $str = sprintf("%s, %s(%d) >> %s\n", $dt$func$line$msg);
0275:     fwrite($outfp$str);
0276: 
0277:     return fclose($outfp);
0278: }

本プログラムでは、データベースや外部APIを使うことなどから、予想外のトラブルが発生することが予想される。そこで、アプリケーションログをファイルに書き出すメソッド putAppLog を用意した。ログはテキストファイルで、1行=1ログである。
引数として、ログに書き出すメッセージ、出力レベル、行番号、関数名を与える。行番号は定数 __LINE__、関数名は定数 __FUNCTION__ で、それぞれ与えるといいだろう。

ログに付与するタイムスタンプは、 date  で生成する。引数に DATE_W3C を指定することで、W3C形式の年月日時分秒からなる文字列を生成する。

ログファイルが無ければ、ディレクトリとパスを自動生成する。
なお、ログファイル名は、実行プログラム名の拡張子を '.log' に置換したものとなる。保存するディレクトリは、変数 $fname_mylog に指定しておく。

ユーザーID取得+DB登録

0281: /**
0282:  * ユーザーID取得+DB登録
0283:  * @param   string $screen_nameスクリーンネーム
0284:  * @return  array(ユーザーID, 名前, 作成日時)/array(FALSE,FALSE,FALSE)
0285: */
0286: function getUserID($screen_name) {
0287:     $sql_select = 'SELECT * FROM ' . $this->table_users . ' WHERE screen_name=:screen_name AND active=1';
0288:     $sql_insert = 'INSERT INTO ' . $this->table_users . ' (user_id, screen_name, name, active, dt) VALUES (:user_id, :screen_name, :name, 1, :dt)';
0289: 
0290:     //DB検索
0291:     $stmt = $this->pdo->prepare($sql_select);
0292:     $stmt->bindValue(':screen_name', $screen_namePDO::PARAM_STR);
0293:     $ret = $stmt->execute();
0294:     $row = $stmt->fetch();
0295:     if (isset($row['user_id'])) {
0296:         $userID = $row['user_id'];
0297:         $name   = $row['name'];
0298:         $dt     = $row['dt'];
0299: 
0300:     //DB登録
0301:     } else {
0302:         //TwitterAPI:ユーザー取得
0303:         $results = $this->ptw->users($screen_name);
0304:         //エラー処理
0305:         if ($this->ptw->iserror()) {
0306:             $this->error  = TRUE;
0307:             $this->errmsg = 'Error TwitterAPI: ' . $screen_name . ' -- ' . $this->ptw->geterror();
0308:             $this->putAppLog($this->errmsg, 0, __LINE____FUNCTION__);
0309:             $userID = FALSE;
0310:             $name = FALSE;
0311:             $dt = FALSE;
0312:         //正常処理
0313:         } else {
0314:             $msg = 'Success TwitterAPI: ' . $screen_name . ' -- get user_id.';
0315:             $this->putAppLog($msg, 1, __LINE____FUNCTION__);
0316:             $userID = $results['id'];
0317:             $name   = $results['name'];
0318:             $dt     = $results['created_at'];
0319:             $stmt = $this->pdo->prepare($sql_insert);
0320:             $stmt->bindValue(':user_id', $userIDPDO::PARAM_STR);
0321:             $stmt->bindValue(':screen_name', $screen_namePDO::PARAM_STR);
0322:             $stmt->bindValue(':name', $namePDO::PARAM_STR);
0323:             $stmt->bindValue(':dt', date(DATE_W3Cstrtotime($dt)), PDO::PARAM_STR);
0324:             $ret = $stmt->execute();
0325:             //エラー処理
0326:             if ($ret == 0) {
0327:                 $this->error = TRUE;
0328:                 $this->errmsg = 'Error SQLite: ' . $screen_name . ' -- cannot add user.';
0329:                 $this->putAppLog($this->errmsg, 0, __LINE____FUNCTION__);
0330:                 $userID = FALSE;
0331:                 $name = FALSE;
0332:                 $dt = FALSE;
0333:             } else {
0334:                 $msg = 'Success SQLite: ' . $screen_name . ' -- add user.';
0335:                 $this->putAppLog($msg, 1, __LINE____FUNCTION__);
0336:             }
0337:         }
0338:     }
0339: 
0340:     return array($userID$name$dt);
0341: }

メソッド getUserID は、与えられたスクリーンネームをキーにデータベースを検索し、対応するユーザーIDを返す。
もしスクリーンネームが登録されていなければ TwitterPI を呼び出し、検索をかけ、その結果をデータベースに登録する。

ユーザーの最小/最大ツイートIDを取得

0381: /**
0382:  * ユーザーの最小/最大ツイートIDを取得
0383:  * @param   string $screen_nameスクリーンネーム
0384:  * @return  array(最小ID, 最大ID)
0385: */
0386: function getMinMaxID($screen_name) {
0387:     $sql_select = 'SELECT MIN(id), MAX(id) FROM ' . $this->table_tweets . ' WHERE user_id=:user_id';
0388: 
0389:     list($userID$dt) = $this->getUserID($screen_name);
0390:     if ($userID == FALSE)   return array(FALSEFALSE);
0391: 
0392:     //DB取得
0393:     $stmt = $this->pdo->prepare($sql_select);
0394:     $stmt->bindValue(':user_id', $userIDPDO::PARAM_STR);
0395:     $ret = $stmt->execute();
0396:     $row = $stmt->fetch();
0397:     //エラー処理
0398:     if (! isset($row[0])) {
0399:         $this->error = TRUE;
0400:         $this->errmsg = 'Error SQLite: ' . $screen_name . ' -- cannot get min/max ID.';
0401:         $this->putAppLog($this->errmsg, 0, __LINE____FUNCTION__);
0402:         return array(FALSEFALSE);
0403:     //正常リターン
0404:     } else {
0405:         $msg = 'Sucess SQLite: ' . $screen_name . ' -- get min/max ID.';
0406:         $this->putAppLog($msg, 2, __LINE____FUNCTION__);
0407:         return array($row[0]$row[1]);
0408:     }
0409: }

メソッド getMinMaxID は、与えられたスクリーンネームをキーにデータベースを検索し、対応するユーザーの最小/最大ツイートIDを取得する。
これは、メソッド getUserTweets で複数ツイートを取得する際、その基準ID――新しいメッセージを取得したいなら最大IDを、古いメッセージより最小IDを――を指定してやる必要があるからである。

ユーザーのツイートを取得+DB登録

0674: /**
0675:  * ユーザーのツイートを取得+DB登録
0676:  * @param   string $screen_nameスクリーンネーム
0677:  * @param   int    $count取得ツイート数
0678:  * @param   string $since_idこのIDより新しいツイートを取得[省略可能]
0679:  * @param   string $max_id  このIDより古いツイートを取得[省略可能]
0680:  * @return  int 登録数/FALSE
0681: */
0682: function getUserTweets($screen_name$count$since_id='', $max_id='') {
0683:     $url    = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
0684:     $method = 'GET';
0685:     $param['screen_name'] = $screen_name;
0686:     $param['exclude_replies'] = 'false';
0687:     $msg = '';
0688:     if ($since_id != '') {
0689:         $param['since_id'] = $since_id;
0690:         $count++;
0691:         $msg = 'after ID' . $since_id;
0692:     } else if ($max_id != '') {
0693:         $param['max_id']   = $max_id;
0694:         $count++;
0695:         $msg = 'before ID' . $max_id;
0696:     }
0697:     $param['count'] = $count;
0698: 
0699:     //API呼び出し間隔確認
0700:     if ($this->getElapsedTime() <= TIME_INTERVAL) {
0701:         $this->error = TRUE;
0702:         $this->errmsg = 'TwitterAPI: too many request';
0703:         return FALSE;
0704:     }
0705:     //ツイート取得
0706:     $ret = $this->ptw->request_user($url$method$param);
0707:     $count = 0;
0708:     //エラー処理
0709:     if (($ret == FALSE|| (count($this->ptw->responses) == 0)) {
0710:         $this->error = TRUE;
0711:         $this->errmsg = 'Error TwitterAPI: ' . $screen_name . ' -- cannnot get tweets.';
0712:         $this->putAppLog($this->errmsg, 0, __LINE____FUNCTION__);
0713:     //ツイートをDB登録
0714:     } else {
0715:         $this->addLogAPI($url$count);   //TwitterAPIアクセス記録を更新
0716:         foreach ($this->ptw->responses as $item) {
0717:             $id = $item->id_str;
0718:             $userID = $item->user->id_str;
0719:             $dt = $item->created_at;
0720:             $description = $item->text;
0721:             if ($this->addTweet($id$userID$dt$description) == FALSE)   return FALSE;
0722:             $count++;
0723:         }
0724:         $msg = 'Success TwitterAPI: ' . $screen_name . ' -- get ' . $count . ' tweets ' . $msg . '.';
0725:         $this->putAppLog($msg, 1, __LINE____FUNCTION__);
0726:     }
0727:     return $count;
0728: }

メソッド getUserTweets は、与えられたスクリーンネームをキーに TwitterAPI を呼び出し、$count で指定された数だけツイートを取り出し、データベースに格納する。

なお、TwitterAPI は同時に大量の呼び出しができないため、メソッド getElapsedTime を使い、あらかじめ設定されたインターバル TIME_INTERVAL より短ければ、API呼び出しを行わないように制御している。

TwitterAPI 呼び出し待ち

0655: /**
0656:  * 前回のTwitterAPIアクセスからの経過時間を取得
0657:  * @return  int 経過時間(秒)
0658: */
0659: function getElapsedTime() {
0660:     $sql_select = 'SELECT dt FROM ' . $this->table_logAPI . ' ORDER BY dt DESC';
0661: 
0662:     //DB取得
0663:     $stmt = $this->pdo->prepare($sql_select);
0664:     $ret = $stmt->execute();
0665:     $row = $stmt->fetch();
0666:     if ($ret == 0) {
0667:         return 99999;
0668:     } else {
0669:         $dt = strtotime($row['dt']);
0670:         return time() - $dt;
0671:     }
0672: }

メソッド getElapsedTime を使い、あらかじめ設定されたインターバル TIME_INTERVAL より短ければ、API呼び出しを行わないように制御していると書いたが、クライアント側でも制御をかけている。
クライアント側では JavaSript を使ったカウントダウンタイマを定義し、経過時間を超えるまで、ボタンを disabled にしている。

自動実行

本プログラムは、OSが用意する自動実行の仕組みを利用し、定期的に学習データを取得・格納することを想定している。
ここでは、Windows タスクスケジューラに登録し、1時間ごとに実行する方法を紹介する。
PHPで機械学習(その1):ツイートを取得しDB格納
タスクスケジューラを起動したが、右ペインのメニューから「基本タスクの作成」を選ぶ。すると、左図のようなウィザードが立ち上がる。
まず、タスクの名前と説明を記入する。
PHPで機械学習(その1):ツイートを取得しDB格納
タスクの開始(トリガー)は「毎日」を選ぶ。
PHPで機械学習(その1):ツイートを取得しDB格納
続いて「開始日時」「間隔」を指定する。
PHPで機械学習(その1):ツイートを取得しDB格納
操作については、「プログラムの開始」を選ぶ。
PHPで機械学習(その1):ツイートを取得しDB格納
プログラム/スクリプトには、"php.exe" をフルパスで指定する。
引数の追加には、実行する "BayesClassifier.phop" をフルパスで指定する。
開始には、カレントディレクトリをフルパスで指定する。実行する "BayesClassifier.phop" と同じディレクトリを指定しておけばいい。
PHPで機械学習(その1):ツイートを取得しDB格納
これでウィザードは終了し、タスクスケジューラに BayesClassifier が登録される。
PHPで機械学習(その1):ツイートを取得しDB格納
タスク一覧か BayesClassifier のプロパティを表示させ、トリガーの編集を行う。
繰り返し間隔を「1時間」とすることで、開始時刻から1時間ごとに、本プログラムが実行されるようになる。

参考サイト

(この項おわり)
header