プログラム

JavaScriptで文字列を切り詰める。prototype.jsを使って。

Ethnaの組み込みSmartyプラグインの "{...|truncate_i18n}"の様な処理をJavaScript側でできないかな。と、思っていたらprototype.jsのStringクラスの拡張でメソッドが用意されていた。

truncate([length = 30[, suffix = '...']]) -> string
Truncates a string to the given length and appends a suffix to it (indicating that it is only an excerpt).

http://www.prototypejs.org/api/string/truncate

なので、万事解決。
リファレンスよく読まないとだめ...。

Ethna_Filterを継承したfilterでログを取る

ささやかにメモ。
Ethnaのフィルタクラスでログを取る場合、Ethna_Filterが継承されており、メンバに$loggerってのがいるので、それを使えばよい。
postFilterメソッドの中とかで $this->backend->getLogger() などとうっかりやってしまった。

PEAR::Cache_Lite_Function

以前も書いたPEARライブラリに登録されているキャッシュ処理のクラスに関数の呼び出し結果をそのままキャッシュできるものも含まれている事に気づいたので使ってみた。
メモリベースのキャッシュ機構がメジャーなのだろうけど、いま仕事で扱っている程度であればディスクキャッシュでも十分ということで。
とある箇所で、操作していると"つっかかる感"があって、その場所を調べていくとDBと連動してごにょごにょしているところであるので導入したが、結構効果があった。
メソッドの呼び出し方を変えるだけなので比較的楽に使えてみんなハッピー。

詳細はPEARのドキュメントを見てもらう事にしてメモがてら、サンプルを書いておく。*1

<?php
require_once('Cache/Lite/Function.php');
class CacheTest{
var $_cacheopts;
function CacheTest(){
$this->_cacheopts = array(
"lifeTime"               => 3600,// cacheの有効時間(秒)
"hashedDirectoryLevel"   => 2,  // cache階層
"debugCacheLiteFunction" => true,// デバッグ出力 (標準出力に "Hit" or "fail"表示)
);
}
function test($param = 0){
$cache  =& new Cache_Lite_Function($this->_cacheopts);
// 自身のメソッド"process"を呼び出す。引数は$param
$result = $cache->call(array(&$this, "process"), $param);
return $result;
}
/**
  * 比較的重い処理 (1度呼び出した結果を保管したい対象の処理)
  * @param $init 
  */
function process($init){
$resp = $init;
for($i=0; $i<5000000; $i++){
$resp++;
}
return $resp;
}
}
$cachetest = new CacheTest();
// 開始時間メモ
$stime = explode(' ', microtime());
$stime = $stime[1] + $stime[0];
print "start = ".$stime."\n";
// 処理実行
$resp = $cachetest->test($argv[1]);
// 終了時間から実行時間算出
$etime = explode(' ', microtime());
$etime = $etime[1] + $etime[0];
$time   = round(($etime - $stime), 4);
printf("%f seconds\n", $time);
printf("CacheTest::test response = %s\n", $resp);
?>

な具合である。
ここでは例として、コマンドライン引数で渡した値を初期値としてそこから500万回インクリメントするという重い処理の結果をキャッシュさせている。
で、これを実行すると1回目はキャッシュが効かないので普通に実行される。

/Users/hideack/tmp% php -q test.php 10
start = 1238689332.75
Cache missed !
1.634000 seconds
CacheTest::test response = 5000010

次にもう一度同じことを実行してみる

/Users/hideack/tmp% php -q test.php 10
start = 1238689377.62
Cache hit !
0.001300 seconds
CacheTest::test response = 5000010

今度はキャッシュから引かれたので実行時間が大幅に短くなっている。
こんな具合。

あぁ、久しぶりに技術的なこと書いた。

*1:もっともサンプルを書いて一番役に立てているのは自分かもしれない...。良く忘れてここを見て振り返る。

PHPでNaive Bayesを使ってみる

今月号のWEB+DB PRESS

WEB+DB PRESS Vol.49

WEB+DB PRESS Vol.49

はてなブックマークのリニューアルに際しての特集記事があったり、レコメンドエンジンの解説記事があったりと非常に読み応えがあっていつもの3割増でおすすめ。
で、ブックマークのカテゴリ自動判定システムで使われているアルゴリズムはComplement Naive Bayesで、このアルゴリズムの元となっているアルゴリズムはNaive Bayes(単純ベイズ分類機)と呼ばれるもの。
Perlでは、記事でも紹介されている通り、Algorithm::NaiveBayesというライブラリがCPANにあるので利用するとアルゴリズムが比較的簡単に利用することができる。
このアルゴリズムを使ってみたいと思ったのだけど、あいにくPHPでは似た形で利用できるライブラリがすぐに見つからなかったので、突貫でこのPerlのライブラリを移植してみた。
Perl版だと、スコアを計算する方法を"frequency", "discrete", "gaussian"の3通りから選べたり、学習させた結果を保管できるのだけど、このたびのものは無し。
記事に記載のサンプルに倣って試してみる。
PHP実装もPerlのインターフェースに併せている。addInstanceメソッドの第一引数に学習対象となる文書の単語の出現数をarrayで与え、その文書が所属するカテゴリを第2引数に与える。
trainメソッドで学習を実行して、predictメソッドで分類を推定する文書中の単語とその出現数を与えると、カテゴリに所属する確率を推測してくれる。

<?php
$bayes = new NaiveBayes();
$bayes->addInstance(array("はてな" => 5, "京都" => 2), array("it"));
$bayes->addInstance(array("引っ越し" => 1, "" => 1), array("life"));
$bayes->train();
$resp = $bayes->predict(array("はてな" => 1, "引っ越し" => 1, "京都" => 1));
print_r($resp);
?>

上記のソースを実行すると、

Array
(
[it] => 0.825130233192
[life] => 0.564942561923
)

Perl版と同じ結果になるので大丈夫かな。。。と。
この場合だと、ITというカテゴリに所属する確率が高いと判定されたということになる。
ソースコードは以下参照。おそらく逐次直します。変なところもあるだろうし。きっと。

<?php
/**
 * Naivebayes.php 
 *
 * This package was ported from Perl's Algorithm::NaiveBayes (frequency model only)
 * http://search.cpan.org/~kwilliams/Algorithm-NaiveBayes-0.04/lib/Algorithm/NaiveBayes.pm
 * 
 * @category  Algorithm 
 * @package   Naivebayes
 * @author    hideack
 * @license   http://www.php.net/license/3_01.txt The PHP License, version 3.01
 * @version   0.1 
 */
class Naivebayes{
private $modeltype;
private $instances;
private $trainingdata;
private $model;
public function __construct(){
$this->trainingdata = array(
"attributes" => array(),
"labels"     => array(),
);
$this->instances = 0;
$this->modeltype = "";	// Perl版では切り替え可能
}
public function addInstance($attributes, $label){
$this->instances++;
foreach($attributes as $keyword => $count){
if(isset($this->trainingdata['attributes'][$keyword])){
$this->trainingdata['attributes'][$keyword] += $count;
}
else{
$this->trainingdata['attributes'][$keyword] = $count;
}
}
foreach($label as $labelword){
if(isset($this->trainingdata['labels'][$labelword]['count'])){
$this->trainingdata['labels'][$labelword]['count']++;
}
else{
$this->trainingdata['labels'][$labelword]['count'] = 1;
}
foreach($attributes as $keyword => $count){
if(isset($this->trainingdata[$keyword])){
$this->trainingdata['labels'][$labelword]['attributes'][$keyword] += $count;
}
else{
$this->trainingdata['labels'][$labelword]['attributes'][$keyword] = $count;
}
}
}
}
public function train(){
$m = array();
$labels = $this->trainingdata['labels'];
$m['attributes'] = $this->trainingdata['attributes'];
$vocab_size = count($m['attributes']);
foreach($labels as $label => $info){
$m['prior_probs'][$label] = log($info['count'] / $this->instances);
$label_tokens = 0;
foreach($info['attributes'] as $word => $count){
$label_tokens += $count;
}
$m['smoother'][$label] = -log($label_tokens + $vocab_size);
$denominator = log($label_tokens + $vocab_size);
foreach($info['attributes'] as $attribute => $count){
$m['probs'][$label][$attribute] = log($count + 1) - $denominator;
}
}
$this->model = $m;
}
public function predict($newattrs){
$scores = $this->model['prior_probs'];
foreach($newattrs as $feature => $value){
foreach($this->model['probs'] as $label => $attribute){
$tmpscore = 0.0;
if($attribute[$feature] == 0.0){
$tmpscore = $this->model['smoother'][$label];
}
else{
$tmpscore = $attribute[$feature];
}
$scores[$label] += $tmpscore * $value;
}
}
$scores = $this->rescale($scores);
return $scores;
}
public function labels(){
$labels = array();
foreach($this->trainingdata['labels'] as $label => $value){
$labels[] = $label;
}
return $labels;
}
public function doPurge(){
// 未実装...
}
private function rescale($scores){
$total = 0;
$max  = max($scores);
$rescalescore = $scores;
foreach($rescalescore as $key => $val){
$val = exp($val - $max);
$total += pow($val, 2);
$rescalescore[$key] = $val;
}
$total = sqrt($total);
foreach($rescalescore as $key => $val){
$rescalescore[$key] /= $total;
}
return $rescalescore;
}
}
?>

mercurial – その1 MacPortsでインストール

仕事でソースコード管理はSubversionでもVSSでもなくて、CVSを使い続けているのだけど多少は違う形態のものも知っておかないといけないと思っていたので勉強としてmercurialを試すべく自宅PCへ導入をしたので、その際のメモ。

環境はMacOSX10.5 Leopardな訳で、MacPortsを使うとすぐインストールできると見聞きしたので、ターミナルから入力

/Users/hideack% sudo port install mercurial
Password:

      • > Building py25-bz2
      • > Staging py25-bz2 into destroot
      • > Installing py25-bz2 @2.5.4_0
      • > Activating py25-bz2 @2.5.4_0

(中略)

Error: Target org.macports.build returned: shell command " cd "/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_release_ports_python_py25-bz2/work/Python-2.5.4/Modules" && /opt/local/bin/python2.5 setup.py --no-user-cfg build " returned error 1
Command output: usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: setup.py --help [cmd1 cmd2 ...]
or: setup.py --help-commands
or: setup.py cmd --help

error: option --no-user-cfg not recognized

Error: The following dependencies failed to build: py25-bz2 py25-zlib
Error: Status 1 encountered during processing.

早速止まる。海外のユーザのTwitterの叫びを見つけて見るところ、"Pythonのバージョンが古いのが原因だ!"と一言書いてあったので、素直に信じて

/Users/hideack% sudo port upgrade python25

でアップグレードしてみるもNG。
ふと気づくと、どうもMacPortsで入れているPythonではなくて、/usr/binにあるPythonを参照してしまっているのが原因(?)の様だ。
/usr/bin/python を一旦リネームしてみると、

/% which python
/opt/local/bin/python

となって、Portsでいれた場合のパスになっている。
再度、mercurialMacPorts経由でインストールしてみる。

/Users/hideack% sudo port install mercurial
Password:

      • > Building py25-bz2
      • > Staging py25-bz2 into destroot

(中略)

無事終了。
確認してみる。

/Users/hideack% hg --version
Mercurial Distributed SCM (version 1.1.2)

Copyright (C) 2005-2008 Matt Mackall and others
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

準備完了である。
さぁ、使ってみようかな...。

XCodeのバージョンアップ

愛用しているMacの開発環境一式はXcodeに依存している訳ですが、今使っているものは同梱のDVDに入っているものだったので多少バージョンが古い事に気づく。
f:id:hideack:20090225000016j:image
f:id:hideack:20090225000017p:image
ということで、Appleのサイトから最新のXcodeをダウンロードする。
ダウンロードのURLは以下の通り。

http://developer.apple.com/jp/technology/tools.html

画面左側に "Xcode3 Freedownload" というリンクがあるので、そこからダウンロードを行うことができる。
本日現在のバージョンは3.1.2
1Gbyte近いディスクイメージがダウンロードされるので、後はマウントしてインストールするのみ。
ダウンロードする際にはAppleのIDが必要。あと、勤務先の名前が未記入だと記入を求められる。*1
加えて開発している環境とかもアンケートされる。
Safari使ってるか?とか、マシンはWindowsかMacか?とか。

*1:漢字が文字化けしていたのだが問題なかったのだろうか

はてなダイアリーAtomPubをPHPで使う

はてなダイアリーAtomPubを使ってみた。PEARの中にもServices_Hatenaや、Do You PHP はてなの"mixiのあしあとAPI"の使い方を参照にしつつ。
Service_Hatenaはダイアリーの対応はまだの様だったので勉強がてら、PHP5専用で一から書いてみた。

<?php
require_once('HTTP/Request.php');
class HatenaDiaryStore{
private $hatenaid;
private $hatenapw;
public function __construct(){
}
public function __set($name, $value){
switch($name){
case "hatenaid": $this->hatenaid = $value; break;
case "hatenapw": $this->hatenapw = $value; break;
}
}
public function postNewEntry($title, $body){
$reqxml = sprintf('<?xml version="1.0" encoding="utf-8"?><entry xmlns="http://purl.org/atom/ns#"><title>%s</title><content type="text/plain">%s</content></entry>',
$title,
$body
);
$this->postNewEntryWithXml($reqxml);
}
public function postNewEntryWithXml($xml){
$url = sprintf("http://d.hatena.ne.jp/%s/atom/blog", $this->hatenaid);	// Hatena diary collection URL
$wsseval = $this->getWsseHeaderValue();
// HTTP Request start.
$httpreq = new HTTP_Request($url);
$httpreq->setMethod(HTTP_REQUEST_METHOD_POST);
$httpreq->addHeader('X-WSSE', $wsseval);
$httpreq->addRawPostData($xml);
if (PEAR::isError($httpreq->sendRequest())) {
throw new Exception("http request error");
}
if($httpreq->getResponseCode() != 201){
throw new Exception("bad request published");
}
}
private function getWsseHeaderValue(){
$created = gmdate('Y-m-d\TH:i:s\Z');
$nonce   = base64_encode(sha1(mt_rand()));
$pwdigest  = base64_encode(pack('H*', sha1($nonce.$created.$this->hatenapw)));
$xwsseval = sprintf(
'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$this->hatenaid,
$pwdigest,
$nonce,
$created
);
return $xwsseval;
}
}
$title = "てすと";
$body  = <<<EOF
気分転換でごにょごにょ中。あは。
-テスト...テスト...ただいまテスト...
-RSSリーダ購読されている方、ご了承ください。
EOF;
$hatena = new HatenaDiaryStore();
$hatena->hatenaid = "hideack";
$hatena->hatenapw = "*******";
$hatena->postNewEntry($title, $body);
?>

意外とあっさり書けてびっくり。
先日から投稿してる「今日のひとことたち」は、はてなハイクのフィードを一日の終わり近くに読み取って、
このスクリプトで自動的に投稿させていたり...。ま、他の方法でごにょごにょしてもできそうですが。

fcounter

script.aculo.usを使ってみた事が数度あったのだけど、簡単に何か具体的に作ってみようと言う事でこないだからゴニョゴニョ作った。
単純なカウンタ。このブログにアクセスした記録を自分が利用しているサーバでも取っているので時系列でアクセス量を可視化するという極シンプルなものです。
アクセス解析を作るのが目的ではなくて、ライブラリを触ってみる方が本題なので細かい事は気にされぬ様。
今朝から見ていたDVDの影響で'f'でアクセス数を表現してみました。24時間前から1時間毎のアクセスが画面に表示される'f'の数です。
このブログのアクセス数はたかが知れているので問題ないかと。

fcounter
f:id:hideack:20090208164319p:image
http://www.feeddown.com/fcounter/

ただ、目下Internet Explorerだと何故か'Effect.Opacity'がうまくいかない。
prototype.jsのPeriodicalExecuterで毎秒画面の表示を更新しているのだけど、'f'の表示が消えてしまう。
なので、IEだと表示される字がスッと消えるエフェクトが効いていません。あしからず。
Firefox, Webkit系であれば大丈夫なんだけど。
うーむ、修行が足りない。

VHDLで三項演算子

正確にはVHDLだと、条件付信号代入文か。セレクタだな。
たまにしか使わないので、すぐに忘れる。

s_out <= s_in1 when s_state = '1' else s_in2;

あと、一致は"="だったな。"=="の様に重ねない。

改めて自分はなんでもやってて、何にもできてないなぁ。
これ読んで一層沈んだぞ。
希望は持とう。希望は。

Ethna_AppObject(まだ意見無し…)

昨日、折角コメント頂けたので少し考えてみようかな。と今朝から思っていたりしています。*1
http://ethna.jp/ethna-dblayer-discuss.html を見るとちょうど話題になっていたんだ..。
あと、あぁ、以前何気にちゃっかり読んでいた焼き肉会議の議事録にもちゃんと最初の項目に書いてあった。

もっとも、LSI設計屋が何故かVC++でWindowsアプリを作る様になって、そこからWebアプリに手を出したという非常に変な経歴のため肝心な知識が「ガバッ」と抜けていたりするのでナニな可能性も大ですが。
ただ、Ethnaが無かったり、Web上に溢れてるあちこちのblog等々で書かれている情報が無かったら、どうにもこうにも仕事にならなかったのは事実なので、多少なりとも今回のことのみならず情報発信してお役に立てたらな。と思う次第。

*1:あぁ、書いてしまったけど何も書けないかもしれないが...