目次
サンプル・プログラムの実行例
サンプル・プログラム
BayesClassifier.php | サンプル・プログラム |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
サンプル・プログラムの流れ
コンストラクタでは、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');
あるユーザーのツイートを同時に複数取得するが、その最大数を決めるのが 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_ERRMODE, PDO::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: }
コンストラでは、前述のオブジェクト pahooTwitterAPI を生成する。
続いてデータベース SQLite の準備を行う。データベースがなければ、自動的に生成する。
テーブル定義
$table_users:ユーザー情報 | |||
---|---|---|---|
No. | 名前 | 型 | 内容 |
1 | user_id | テキスト | ユーザーID |
2 | screen_name | テキスト | スクリーンネーム |
3 | dt | テキスト | 登録日時 |
$table_tweets:ツイート内容 | |||
---|---|---|---|
No. | 名前 | 型 | 内容 |
1 | id | テキスト | ツイートID |
2 | user_id | テキスト | ユーザーID |
3 | description | テキスト | ツイート内容 |
4 | flag | 整数 | 学習済みフラグ |
5 | dt | テキスト | 登録日時 |
$table_vectors:学習記録 | |||
---|---|---|---|
No. | 名前 | 型 | 内容 |
1 | id | テキスト | 管理ID |
2 | user_id | テキスト | ユーザーID |
3 | word | テキスト | 単語 |
4 | count | 整数 | 出現回数 |
5 | dt | テキスト | 登録/更新日時 |
$table_logAPI:TwitterAPI利用ログ | |||
---|---|---|---|
No. | 名前 | 型 | 内容 |
1 | id | 整数 | 管理ID |
2 | url | テキスト | TwitterAPI URL |
3 | count | 整数 | 取得ツイート数 |
4 | dt | テキスト | 利用日時 |
アプリケーション・ログ
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_mylog) return TRUE;
0256:
0257: //タイムスタンプ
0258: $dt = date(DATE_W3C, time());
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: }
引数として、ログに書き出すメッセージ、出力レベル、行番号、関数名を与える。行番号は定数 __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_name, PDO::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', $userID, PDO::PARAM_STR);
0321: $stmt->bindValue(':screen_name', $screen_name, PDO::PARAM_STR);
0322: $stmt->bindValue(':name', $name, PDO::PARAM_STR);
0323: $stmt->bindValue(':dt', date(DATE_W3C, strtotime($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: }
もしスクリーンネームが登録されていなければ 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(FALSE, FALSE);
0391:
0392: //DB取得
0393: $stmt = $this->pdo->prepare($sql_select);
0394: $stmt->bindValue(':user_id', $userID, PDO::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(FALSE, FALSE);
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: }
これは、メソッド 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: }
なお、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: }
クライアント側では JavaSript を使ったカウントダウンタイマを定義し、経過時間を超えるまで、ボタンを disabled にしている。
自動実行
ここでは、Windows タスクスケジューラに登録し、1時間ごとに実行する方法を紹介する。
まず、タスクの名前と説明を記入する。
引数の追加には、実行する "BayesClassifier.phop" をフルパスで指定する。
開始には、カレントディレクトリをフルパスで指定する。実行する "BayesClassifier.phop" と同じディレクトリを指定しておけばいい。
繰り返し間隔を「1時間」とすることで、開始時刻から1時間ごとに、本プログラムが実行されるようになる。
参考サイト
- PHPで機械学習(その1):ツイートを取得しDB格納:ぱふぅ家のホームページ
- PHPで機械学習(その2):ツイート内容を学習:ぱふぅ家のホームページ
- PHPで機械学習(その3):単純ベイズ分類機:ぱふぅ家のホームページ
- PHPとデータベース:ぱふぅ家のホームページ
- PHPでTwitterに投稿(ツイート)する:ぱふぅ家のホームページ
- 機械学習とは:sas
- ベイズの定理:高校数学の美しい物語
- ディープラーニングの無料学習講座:浜村拓夫の世界
- 数式無しで理解する機械学習(エンジニア向け):情強志向
- VPSサーバーにChainerをインストールしたらディープラーニングを超簡単に試せた:ゆるりの足あと
機械学習には、あらかじめ与える学習データの質と量が大切だが、ネット時代になり、家にいながらにして学習データが集められるようになった。
そこで今回は、PHPを使って機械学習の基本について紹介することにしよう。
機械学習は、大まかに分けて3つの段取りを踏む。
学習データは、複数ユーザのツイートを使うことにする。
今回は、まず、複数ユーザのツイートを学習デートして収集し、データベースに格納するところまでを紹介する。
(2021年7月11日)PHP8対応,リファラ・チェック改良