phpによるベイジアン

2年ぐらい前に作ったphpで作ったベイジアンクラス(ベイズ/ライブラリ)を休日にスクラッチから書き直したので公開してみる。

ライセンス:ご自由にお使いください。
大切なこと:バグがあっても泣かない。

<?
	//myベイズ ライブラリ
	class MyBayes
	{
		var $D = array();
		var $ALL = array();
		/*
		array(
		'カテゴリ名'		=> array('e' => このカテゴリに含まれる数
							,  'w' => array("単語1" => このカテゴリーのこの単語が表れた数
										,	"単語2" => このカテゴリーのこの単語が表れた数
											)
							) 
		);
		ALL				=> array('e' => すべてのカテゴリに登場した数
							,  'w' => array("単語1" => この単語がすべてのカテゴリーで登場した数
										,	"単語2" => この単語がすべてのカテゴリーで登場した数
											)
							) 
		*/
		var $LastWordCount = array();
		
		function MyBayes()
		{
			//総計を取るカテゴリーは絶対に存在していないと困る.
			$this->ALL = array('e'=>0,'w'=>array());
		}
		
		function Train($category , $wordsArray)
		{
			//新設したカテゴリ?
			if (!isset($this->D[$category]))
			{
				$this->D[$category] = array('e'=>0,'w'=>array());
			}
			//カテゴリに登場した件数を増やす.
			@$this->D[$category]['e'] ++;
			@$this->ALL['e'] ++;

			//単語が表れた回数を増やす.
			foreach($wordsArray as $word)
			{
				//単語が表れた回数を増やす.
				@$this->D[$category]['w'][$word]++;
				@$this->ALL['w'][$word]++;
			}
			return true;
		}
		
		function UnTrain($category , $wordsArray)
		{
			if (!isset($this->D[$category]))
			{
				return false;
			}
			//カテゴリに登場した件数を減らす.
			@$this->D[$category]['e'] --;
			@$this->ALL['e'] --;

			//単語が表れた回数を減らす.
			foreach($wordsArray as $word)
			{
				//単語が表れた回数を減らす.
				@$this->D[$category]['w'][$word]--;
				@$this->ALL['w'][$word]--;
			}
			return true;
		}

		//カテゴリ推測(ポールグラハムエンジン)
		function EngineByPaulGraham($wordsArray)
		{
			//個数を数えます.
			$wordCount = array();
			foreach($wordsArray as $word)
			{
				@$wordCount[$word]['count'] ++;
			}
			$scores = array();

			//単語毎に確率を計算します.
			foreach(array_keys($this->D) as $category)
			{
				foreach(array_keys($wordCount) as $word)
				{
					$gi = (double)@$this->ALL['w'][$word] - @$this->D[$category]['w'][$word];
					$bi = (double)@$this->D[$category]['w'][$word];

					$ngood = (double)@$this->ALL['e'] - @$this->D[$category]['e'];
					$nbad = (double)@$this->D[$category]['e'];
					if ($gi <= 0 && $bi <= 0)
					{
						$p = 0.5;
					}
					else
					{
						$p = ($bi / $nbad) /  ( ($gi / $ngood) + ($bi / $nbad) );
						// 0.01 <= $p <= 0.99 に収める.
						$p = min(max($p,0.01),0.99);
					}

					@$wordCount[$word][$category]['p'] = $p;
				}

				//このカテゴリである確立を求めます.
				//単語の確立を組み合わせて複合確立を求めます.
				$gp = $bp = 1;
				foreach(array_keys($wordCount) as $word)
				{
					$gp *= $wordCount[$word][$category]['p'];
					$bp *= (1 - $wordCount[$word][$category]['p']);
				}
				$scores[$category] = $gp / ($gp + $bp);
			}
			$this->LastWordCount = $wordCount;
			return $scores;
		}

		//カテゴリ推測(ポールグラハムエンジン改)
		//ポールグラハムエンジンの弱点としてまれにしか登場しない単語の確率が99%等にはねが上がってしまう問題がある。
		//そこで、マレにしか登場しないのに確率が高い単語があったら重しをつけて確立を下げる。(結構強引)
		function EngineByPaulGrahamKAI($wordsArray)
		{
			//出現頻度が低い単語につける重し
			$omoshiArray = array( 0=>0.25, 1=>0.20,2=>0.15,3=>0.10,4=>0.5 );
			$omoshiCount = 4;

			//個数を数えます.
			$wordCount = array();
			foreach($wordsArray as $word)
			{
				@$wordCount[$word]['count'] ++;
			}
			$scores = array();

			//単語毎に確率を計算します.
			foreach(array_keys($this->D) as $category)
			{
				foreach(array_keys($wordCount) as $word)
				{
					$gi = (double)@$this->ALL['w'][$word] - @$this->D[$category]['w'][$word];
					$bi = (double)@$this->D[$category]['w'][$word];

					$ngood = (double)@$this->ALL['e'] - @$this->D[$category]['e'];
					$nbad = (double)@$this->D[$category]['e'];
					if ($gi <= 0 && $bi <= 0)
					{
						$p = 0.5;
					}
					else
					{
						$p = ($bi / $nbad) /  ( ($gi / $ngood) + ($bi / $nbad) );

						//強引に重しをつける. この辺の数字は調整が必要!!
						if ($p >= 0.90  && $gi <= $omoshiCount)
						{
							$p = $p - $omoshiArray[$gi];
						}
						else if ($p <= 0.10  && $gi <= $omoshiCount)
						{
							$p = $p + $omoshiArray[$gi];
						}

						// 0.01 <= $p <= 0.99 に収める.
						$p = min(max($p,0.01),0.99);
					}

					@$wordCount[$word][$category]['p'] = $p;
				}

				//このカテゴリである確立を求めます.
				//単語の確立を組み合わせて複合確立を求めます.
				$gp = $bp = 1;
				foreach(array_keys($wordCount) as $word)
				{
					$gp *= $wordCount[$word][$category]['p'];
					$bp *= (1 - $wordCount[$word][$category]['p']);
				}
				$scores[$category] = $gp / ($gp + $bp);
			}

			$this->LastWordCount = $wordCount;
			return $scores;
		}
	};



	//ベイジアンのテスト
	function MyBTest()
	{
		global $L_SETTING;

		$spam  = "AAA AAA AAA BBB DDD";
		$ham   = "ZZZ ZZZ ZZZ BBB DDD";
		$banana= "BANANA";

		$b = new MyBayes();
		$b->Train('spam',explode(" ",$spam));
		$b->Train('ham',explode(" ",$ham));
		$b->Train('banana',explode(" ",$banana));

		$scores = $b->EngineByPaulGraham(explode(" ",$spam));
		assert(round( $scores['spam'] ,3) == 0.997);
		assert(round( $scores['ham'] ,3) == 0.039);
		assert(round( $scores['banana'] ,3) == 0);

		$scores = $b->EngineByPaulGraham(explode(" ",$ham));
		assert(round( $scores['spam'] ,3) == 0.039);
		assert(round( $scores['ham'] ,3) == 0.997);
		assert(round( $scores['banana'] ,3) == 0);

		$scores = $b->EngineByPaulGraham(explode(" ",'CCCA'));
		assert(round( $scores['spam'] ,3) == 0.5);
		assert(round( $scores['ham'] ,3) == 0.5);
		assert(round( $scores['banana'] ,3) == 0.5);

		$scores = $b->EngineByPaulGraham(explode(" ",'BANANA'));
		assert(round( $scores['spam'] ,3) == 0.01);
		assert(round( $scores['ham'] ,3) == 0.01);
		assert(round( $scores['banana'] ,3) == 0.99);

		//データ数が少ないと逆にあまりよくなくなるなぁ。。。
		$scores = $b->EngineByPaulGrahamKAI(explode(" ",$spam));
		assert(round( $scores['spam'] ,3) == 0.923);
		assert(round( $scores['ham'] ,3) == 0.308);
		assert(round( $scores['banana'] ,3) == 0.003);

	}
	set_time_limit(0);
	MyBTest();
	echo "OK";

?>

注意:ボールグラハムと名前が付いていますが、彼のアルゴリズムをガチで実装していません。出現頻度が低い単語を消していないのは手抜きというか、短い文章に対しての評価を目的としたので、こうしています。

EngineByPaulGrahamKAI は、ポールグラハムのアルゴリズムをrtiが強引に改造した邪悪なものです。
ポールグラハムのアルゴリズムでは、出現頻度が極端に偏った単語が 99%などの高確率になってしまうので、そーゆーヤツには強引に重しをつけて確立を沈めています。強引です。もっとスマートな方法がありそうですが、こまけーことはいいんだよって感じ?

現在データは、すべてオンメモリに確保しています。データを保存したい人はクラスごと serialize とかしてください。
形態素解析を行いたい人は、TinySegmenter for PHPとか使うといいんじゃないかなぁと思う。

これはひどい slenium-ideから検証項目表を自動生成

検証をselenium-ideで作っていたが、どーしても検証項目表(テスト表,検証表)がほしいという人がいたので、selenium-ideから検証項目表を自動生成するプログラムを作りました。

事前条件

ファイルの命名規則

テストケースは xxxx 〜こと のように命名すること。
NG ユーザー登録 登録でエラー
OK ユーザー登録できること 登録でエラーにならないこと

カテゴリ名などを先頭につけること

例: ユーザー登録_ユーザー登録できること
バグ_ユーザー登録で1234と入力してもエラーにならないこと

テストは細切れに作ってテストスイートで結合

一つのテストは検証項目表でいう1テスト項目に当たるぐらいの最小単位で作っていく。
後でテストスイートで結合させる。

プログラム

コマンドラインから動作させることを前提としています。
一応webからでも動きますけど。。。

<?

	//selenium の td タグの相当するパラメータを受け取って、
	//非技術者が分かるように日本語化する
	//余計に分かりづらくなるけど細かいところは気にしない.
	//どうせ彼らは何もわか(ry なので数でお茶を濁す.
	function selenium_ide_to_nihongo($base , $t1,$t2,$t3)
	{
		if ( $t1 == "open" )
		{
			$url = $t2;
			if (substr($t2 , 0 , 4) != "http")
			{
				if ( substr($base,-1,1) == "/")
				{
					$url = substr($base,0,strlen($base)-1) . $t2;
				}
				else
				{
					$url = $base . $t2;
				}
			}
			return "URL({$url})にアクセスします。";
		}
		else if ( $t1 == "clickAndWait" )
		{
			return "画面の({$t2})をクリックして次のページに移動します。";
		}
		else if ( $t1 == "click" )
		{
			return "画面の({$t2})をクリックします。";
		}
		else if ( $t1 == "type" )
		{
			return "入力欄 ({$t2})に({$t3})と入力します。";
		}
		else if ( $t1 == "select" )
		{
			return "({$t2})で({$t3})を選択します。";
		}
		else if ( $t1 == "verifyTitle" || $t1 == "assertTitle")
		{
			return "タイトルに ({$t2})と表示されていることを確認します。";
		}
		else if ( $t1 == "verifyTextPresent" || $t1 == "assertTextPresent")
		{
			return "画面に({$t2})と表示されていることを確認します。";
		}
		else if ( $t1 == "verifyTextNotPresent" || $t1 == "assertTextNotPresent" )
		{
			return "画面に({$t2})と表示されていないことを確認します。";
		}
		else if ( $t1 == "verifyValue" || $t1 == "assertValue")
		{
			return "画面の({$t2})に({$t3})と表示されていることを確認します。";
		}
		else if ( $t1 == "verifyNotValue" || $t1 == "assertNotValue")
		{
			return "画面の({$t2})に({$t3})と表示されていないことを確認します。";
		}
		
		//不明なので適当に返す
		if ($t3 == "")
		{
			return "({$t1})によって({$t2})であること";
		}
		else
		{
			return "({$t1})によって({$t2})が({$t3})であること";
		}
	}
	
	//ファイル名をパースします。
	//カテゴリ名_テスト項目
	function selenium_ide_filename_parse($filename)
	{
		$filename = basename($filename);
		$rpos = strrpos($filename,"_");
		if ($rpos === FALSE)
		{
			$category = "";
			$title = $filename;
		}
		else
		{
			$category = substr($filename,0 , $rpos - 1);
			$title = substr($filename, $rpos + 1 );
		}
		
		return 
			array('category' => $category
					,'title' => $title
					);
	}
	
	//ファイルの変更履歴をソース管理から取得する.
	function make_file_history($filename)
	{
		$dir = dirname($filename);

		$s_dir = escapeshellarg($dir);
		$s_filename = escapeshellarg(basename($filename));

		$text = "";
		if ( file_exists("{$dir}/.svn") )
		{//for svn user
			$r = `cd {$s_dir} ; svn log {$s_filename}`;

			//いつ誰が変更したのかだけほしい.
			$regs = array();
			preg_match_all("/(r[0-9]+) \| (.*?) \| (.*?) /us",$r , $regs);

			$count = count($regs[0]);
			for($i = 0 ; $i < $count ; $i ++)
			{
				$text .= "{$regs[2][$i]} {$regs[3][$i]} ({$regs[1][$i]})" . "\r\n";
			}
		}
		return trim($text);
	}

	//selenium ide のテストケースを読み込んで、
	//テストケースで行う内容を日本語にして返します。
	function selenium_ide_to_csv($filename)
	{
		$fileString = file_get_contents($filename);
		if ($fileString === FALSE)
		{
			echo "can not opne : {$fileString}";
			return false;
		}
		$regs = array();

		//ベース
		preg_match('/<link rel="selenium.base" href="(.*?)"/ui',$fileString, $regs);
		$base = $regs[1];

		//td の項目をパース.
		preg_match_all("/<tr>.*?<td>(.*?)<\/td>.*?<td>(.*?)<\/td>.*?<td>(.*?)<\/td>.*?<\/tr>/ums",
			$fileString , $regs);

		$text = "";
		$count = count($regs[0]);
		for($i = 0 ; $i < $count ; $i ++)
		{
			//強引に日本語化
			$text .= selenium_ide_to_nihongo($base , $regs[1][$i],$regs[2][$i],$regs[3][$i]) . "\r\n";
		}
		$text = trim($text);
		
		//テスト作成日と更新日情報をソース管理に問い合わせる
		//鍵交換でノンパスでやり取りできないとフリーズするのでとりあえずコメントアウト
		//$testerText = make_file_history($filename);
		$testerText = "";
		
		//ファイル名からカテゴリとテストの名前を取得.
		$testinfoArray = selenium_ide_filename_parse($filename);
		return		CsvEncode($testinfoArray['category'])	//カテゴリ 
			. ',' . CsvEncode($testinfoArray['title'] )		//テスト名
			. ',' . CsvEncode($text)						//テスト手順と結果
			. ',' . CsvEncode($testerText)					//テスター
			;
	}

	//テストスイート
	function selenium_ide_to_testsuite($filename)
	{
		$fileString = file_get_contents($filename);
		if ($fileString === FALSE)
		{
			echo "can not opne : {$fileString}";
			return false;
		}
		$base = dirname($filename);
		
		//td の項目をパース.
		$regs = array();
		preg_match_all("/<tr>.*?<td><a href=\"(.*?)\">.*?<\/a>.*?<\/td>.*?<\/tr>/ums",
			$fileString , $regs);

		//ヘッダー
		$text=		CsvEncode("カテゴリー")
			. ',' . CsvEncode("テスト名")
			. ',' . CsvEncode("テスト手順と結果")
			. ',' . CsvEncode("テスター")
		 	. "\r\n";

		foreach($regs[1] as $ahref)
		{
			$text .= selenium_ide_to_csv("{$base}/{$ahref}") . "\r\n";
		}
		return trim($text);
	}
	
	//csv形式に変換.
	function CsvEncode($str)
	{
		//エクセルで開くことを前提とするのでsjis化する
		$str = mb_convert_encoding($str,"sjis-win","UTF-8");
		return '"' . str_replace('"','""',$str) . '"';
	}


	//実行!
	//テストスイートを指定します。
	$r = selenium_ide_to_testsuite("testsuite");
	echo $r;

//	$r = make_file_history("/home/rti/svn/aaa/trunk/htdocs/index.php");
//	var_dump($r);
?>

ダイエットペプシ

ダイエットペプシを補充した。
買いにいくのが面倒なので、楽天のドリンク屋さんから10箱まとめ買い(8本入りで1,373円なので結構安いと思う)。
8本 * 10箱 = 80本。

× 80本

重さに直すと、 1.5リットル * 8本 * 10箱 = 120kg

半分ぐらいをワインセラーもどきみたいに並べてみた。

これであと半年は戦える。
ダイエットペプシ or ダイエットコーラが出る蛇口がほしい。