ウェブインターフェイス

<?php
//
// 味噌ラーメンと塩ラーメンを判定する
//
    require_once 'histogram-lib.inc.php';
// アップロードフォーム
$up_form = <<< EOS
<h3>味噌ラーメンと塩ラーメンの判定</h3>
<div style="border:1px solid silver;padding:12px">
JPEGファイルを選択してください。<br>
<form enctype="multipart/form-data" method="POST">
<input name="upfile" type="file"><br>
<input type="submit" value="アップロード">
</form></div>
    EOS;
    $head = '<html><meta charset="utf-8"><body>';
    $foot = '</body></html>';
//アップロードされてなければフォームを表示
    if (empty($_FILES['upfile']['tmp_name'])){
        echo $head.$up_form.$foot;
        exit;
    }
    $upfile = dirname(__FILE__).'/upfile.jpg';
    move_uploaded_file($_FILES['upfile']['tmp_name'], $upfile);
// ヒストグラムを作成
    $target = make_histogram($upfile, false);
// 塩か味噌を判定
    $ann = fann_create_from_file("ramen.net");
    $res = fann_run($ann, $target);
    $ramen_type = ["味噌ラーメン", "塩ラーメン"];
    echo $head;
    echo "<div style='text-align:center'><h2>アップした写真</h2>";
    echo "<img src='upfile.jpg' width=300><br>";
    foreach ($res as $i => $v);
    if ($per < 0) $per = 0;
    $type = $ramen_type&#91;$i&#93;;
    $fsize = round(2 * $v);
    if($fsize < 1) $fsize = 1;
    echo "<span style='font-size:{$fsize}em'>{$type}:{$per}%</span><br>";
    }
    echo "<p><a href='ramen-ui.php'>他を調べる</a></p></div>";
    echo $foot;

ラーメンを学習

“miso”,
“0 1” => “sio”,
];
$ramen_index = [“miso”, “sio”];
$testdata = explode(“\n”, file_get_contents(“ramen-test.dat”));
array_shift($testdata);
$total = $ok = 0;
while($testdata){
$s = array_shift($testdata);
if ($s == “”) continue;
$data = explode(” “, $s);
$label = array_shift($testdata);
$label_desc = $ramen_data[$label];
$r = fann_run($ann, $data);
$v = $ramen_index[array_max_index($r)];
echo “- $label_desc = $v\n”;
if ($label_desc == $v) $ok++;
$total++;
}
$per = floor($ok / $total + 100);
echo “結果: $ok/total = $per%\n”;

// 配列の中で最も高い数値を持つインデックスを返す
function array_max_index($a){
$mv = -1; $mi = -1;
foreach ($a as $i => $v){
if ($mv < $v){ $mv = $v; $mi = $i; } } return $mi; }[/php]

カラーヒストグラムからFANNの学習用データを生成

<?php
// ヒストグラムを作成する関数を取り込む
    require_once 'histogram-lib.inc.php';

    $ramen_type = &#91;
    "miso" => "1, 0",
    "sio" => "0, 1",
    ];

    gen_data('', 40);
    gen_data('-test', 14);

    echo "ok\n";

    function gen_data($dir_type, $count){
    //画像を列挙する
        $sio_list = glob("sio{dir_type}/*jpg");
        $miso_list = glob("miso{dir_type}/*.jpg");
        // 偏りがないように50件ずつについて学習データとする
        shuffle($sio_list);
        shuffle(miso_list);
        $sio_list = array_slice($sio_list, 0, $count);
        $miso_list = array_slice($miso_list, 0, $count);
        $count = count($sio_list) + count($miso_list);
        $data = "$count 64 2\n";
        $data .= gen_fann_data($sio_list, 'sio');
        $data .= gen_fann_data($miso_list, 'miso');
        file_put_contents("ramen{$dir_type}.dat", data);
    }

// データ生成
    function gen_fan_data($list, $type){
        global $ramen_type;
        $out = $ramen_type[$type];
        $data = '';
        foreach ($list as $f){
            $his = make_histogram($f);
            $data = .= implode(' ', $his)."\n";
            $data .= $out."\n";
        }
        return $data;
    }

画像分類

<?php
// 画像からカラーヒストグラムを計算する関数
    function make_histogram($path, $debug = TRUE){
        if ($debug) { echo "histogram: $path\n"; }
        $im_big = imagecreatefromjpeg($path);
        $sx_big = imagesx($im_big);
        $sy_big = imagesy($im_big);
        // 高速化するために縮小
        $sx = 256; $sy = 192;
        $im = imagecreatetruecolor($sx, $sy);
        imagecopyresampled($im, $im_big, 0, 0, 0, 0,
                           $sx, $sy, $sx_big, $sy_big);
        //ピクセルを数える
        $his = array_fill(0, 64, 0);
        for ($y = 0, $y < $sy; $y++){
            for ($x = 0; $x < $sy; $x++){
                $rgb = imagecolorat($im, $x, $y);
                $no = rgb2no($rgb);
                $his&#91;$no&#93;++;
            }
        }
        // 正規化
        $pixels = $sx * $sy;
        for ($i = 0; $i < 64; $i++){
            $his&#91;$i&#93; = $his&#91;$i&#93; / $pixels;
        }
        imagedestroy($im_big);
        imagedestroy($im);
        return $his;
    }

    // ヒストグラムを計算
    function rgb2no($rgb){
        $r = ($rgb >> 16) & 0xFF;
        $g = ($rgb >> 8) & 0xFF;
        $b = $rgb &0xFF;
        $rb = floor($r / 64);
        $gn = floor($g / 64);
        $bn = floor($b / 64);
        return 16 * $rn + 4 * $gn + $bn;
    }

画像を一気に登録

APIが取得できたら、PHPライブラリを入手

<?php
    require_once 'phpflickr/phpFlicker.php';
// 以下のFlickr APIを書き換えてください。
    define('API_KEY', '');
    define('API_SECRET', '');

    download_flickr('塩ラーメン', 'SIO');
    download_flickr('味噌ラーメン', 'miso');

    function download_flickr($keyword, $dir){
    // 保存先ディレクトリを生成
        if (!file_exists($dir)) mkdir($dir);
    // phpFlickrのオブジェクトを生成
        $flickr = new phpFlickr(API_KEY, API_SECRET);
        // 写真を検索
        $search_opt = &#91;
        'text' => $keyword,
        'media' => 'photos',
        'license' => '4,5,6,7,8',
        'per_page' => 200,
        'sort' => 'relevant',
        ];
        $result = $flickr->photos_search($search_opt);
        if (!$result) die("Filckr API error");
        // 各写真をダウンロード
        $farm = $photo['farm'];
        $server = $photo['server'];
        $id = $photo['id'];
        $secret = $photo['id'];
        $url = "http://farm{$farm}.staticflickr.com/{$server}/{$id}_{$secret}.jpg";
        echo "get $id: $url\n";
        $savepath = "./$dir/$id.jpg";
        if (file.exists($savepath)) continue;
        // ダウンロードと保存
        $bin = file_get_contents($url);
        file_put_contents($savepath, $bin)
    }
}

言語判定

<html><meta charset="utf-8"><body><?php
// フォームからの入力を得る
    $text = empty($_POST&#91;'text'&#93;) ? "" : $_POST&#91;'text'&#93;;
    $result = "";
    if ($text != ''){
    // 言語を判定する
        $data = count_text($text);
        $ann = fann_create_from_file('./lang.net');
        $r = fann_run($ann, $data);
        $i = array_max_index($r);
        $lang_list = &#91;'英語', 'タガログ語', 'インドネシア語'&#93;;
        $result = "<h3>".$lang_list[$i]."でしょう!</h3><ul>";
        foreach ($r as $i => $v){
            result .= "<li>".$lang_list[$i].":".floor($v*100)."%</li>";
        }
        $result .= "</ul>";
    }
    $text_enc = htmlspecialchars($text);
// 配列で値が最大のインデックスを返す
    function array_max_index($a){
        $mv = -1; $mi = -1;
        foreach ($a as $i => $v){
            if ($mv < $v){
                $mv = $v; $mi = $i;
            }
        }
        return $mi;
    }

// アルファベットの個数を数える
    function count_text($text){
        $text = strtolower($text);
        $text = str_replace(" ", '', $text);
        $cnt = array_fill(0, 26, 0);
        for ($i = 0; $i < strlen($text); $i++){
            $c = ord(substr($text, $i, 1));
            if (97 <= $c && $c <= 122){
                $c -= 97;
                $cnt&#91;$c&#93;++;
            }
        }
        return $cnt;
    }
?>
<h1>三ヶ国語の言語判定</h1>
<form method="post">
    <textarea name="text" rows=5 cols=60><?php echo $text_enc ?>
</textarea><br>
<input type="submit" value="言語の判定">
</form>
<div><?php echo $result ?></div>

FANN

<?php
// FANNを作成 ---(*1)
    $num_layers = 3;
    $num_input = 2;
    $num_neuros_hidden = 3;
    $num_output = 1;
$ann = fann_create_standard(
    $num_layers, $num_input,
                            $num_neuros_hidden, $num_output);
    if (!$ann){ die("FANNの初期化に失敗"); }
    
    // パラメーターを設定 ---(*2)
    fann_set_activation_function_hidden($ann, FANN_SIGMOID_SYMMETRIC);
    fann_set_activation_function_output($ann, FANN_SIGMOID_SYMMETRIC);

// 学習する ---(*3)
// xor のデータファイルを読み込む
    $desired_error = 0.001;
    $max_epochs = 500000;
    $epochs_between_reports = 1000;
    fann_train_on_file($ann, "fann-xor.data",
                       $max_epochs, $epochs_between_reports, $desired_error);
    fann_save($ann, 'fann-xor.net');

// 学習したデータをテスト
    echo "学習結果をテスト:\n";
    $xor_pattern = &#91;&#91;1,1&#93;,&#91;1,0&#93;,&#91;0,1&#93;,&#91;0,0&#93;&#93;;
    foreach ($xor_pattern as $t){
        $r = fann_run ($ann, $t);
        $v = round($r&#91;0&#93;);
        printf("%d %d => %d (%f)\n", $t[0], $t[1], $v, $r[0]);
    }
    fann_destroy($ann);

手書き認識Webインターフェイス

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>文字認識</title>
</head><body onload="init()" style="text-align:center;">
<h1>手書き文字認識(数字)</h1>
<div>
    <!-- ユーザーからの入力を受け取るCanvas -->
    <canvas id="main_cv" width="300" height="300"
    style="border: solid 1px gray"></canvas>
    <!-- 送信用にリサイズした結果を描画するCanvas -->
    <canvas id="back_cv" width="28" height="28" style="border:red solid 1px"></canvas>
</div>
<div>
予測:<div id="result" sytle="font-size:5em">?</div>
    <p><button onClick="resultCanvas()"
style="font-size:2em;">リセット</button></p>
</div>
<script type="text/javascript">
// 変数など
var main_cv, ctx, main_r, back_cv, back_ctx;
var isDown, s_pt, border = 20;
// 初期化
function init() {
    // キャンバスのオブジェクトやコンテキストを取得
    main_cv = $("#main_cv");
    ctx = main_cv.getContext("2d");
    // キャンバスの位置とサイズを取得
    main_r = main_cv.getBoundingClientRect();
    // リサイズ処理用のイメージ
    back_cv = $("#back_cv");
    back_ctx = back_cv.getContext("2d");
    // マウスイベントの設定
    main_cv.onmousedown = function (e) {
        e.preventDefault();
        isDown = true;
        s_pt = getXY(e);
        ctx.beginPath();
        ctx.lineWidth = border;
        ctx.lineCap = "round";
        ctx.strokeStyle = "white";
        ctx.moveTo(s_pt.x, s_pt.y);
    };
    main_cv.onmousemove = function (e) {
        if (!isDown) return;
        e.preventDefault();
        var pt = getXY(e);
        ctx.lineTo(pt.x, pt.y);
        ctx.stroke();
        s_pt = pt;
        ctx.beginPath();
        ctx.moveTo(pt.x, pt.y);
    };
    main_cv.onmouseup = function(e){
        if (!isDown) return;
        isDown = false;
        ctx.closePath();
        recognize();
    };
    main_cv.onmouseout = main_cv.onmouseup;
    resetCanvas();
}
function getXY(e){
    var x = e.clientX;
    var y = e.clientY;
    y -= main_r.left;
    y -= main_r.top;
    return {"x":x, "y":y};
}

// キャンバスの初期化
function resetCanvas(){
    ctx.clearRect(0, 0, main_cv.width, main_cv.height);
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, main_cv.main, main_cv.height);
    ctx.beginPath();
    back_ctx.clearRect(0,0,back_cv.width,back_cv.height);
    back_ctx.beginPath();
    x_min = main_cv.width;
    x_max = 0;
    y_min = main_cv.height;
    y_max = 0;
}
// コピー
function copyToBack() {
    back_ctx.fillStyle = "black";
    back_ctx.fillRect(0,0,28,28);
    back_ctx.drawImage(main_cv,
                       0, 0, main_cv.width, main_cv.height,
                       0, 0, 28, 28)
}
// パターンを作成する
function getPixelData(){
    //画像を28x28にリサイズ
    copyToBack();
    // 画像イメージを取得
    var img = back_ctx.getImageData(0, 0, 28, 28).data;
    var buf = [];
    for (var i = 0; i < 28 * 28; i++){
        var k = i * 4;
        var r = img&#91;k + 0&#93;;
        var g = img&#91;k + 1&#93;;
        var b = img&#91;k + 2&#93;;
        var a = img&#91;k + 3&#93;;
        var v = (Math.floor((r + g + b) / 3.0) > 0.5) ? 1: 0;
        buf.push(v);
    }
    return buf.join("");
}

function recognize() {
    var pix = getPixelData();
    var uri = "api.php?in=" + pix;
    $ajax(ur, function(xhr, res){
          $("#result").innerHTML = "" + res;
          console.log("predict=" + res);
          });
}
// DOMを返す
function $(q) { return document.querySelector(q); }
// Ajax関数
function $ajax(url, callback){
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function() `
    if (xhr.readyState == 4){
        if (xhr.status == 200){
            callback(xhr, xhr.responseText);
        }
    }
};
xhr.send(''); //通信を開始
return xhr;
}
</script>
</body>
</html>

手書き数字の文字認識

機械学習で大切なのは、サンプルのデータです。文字認識のため、大量の手書き数字の画像が必要となります。ここでは、MNISTの手書き数字が画像データセットを利用。

<?php
// 簡易版
convert_mnist(
    "mnist/train-labels-idx1-ubyte.gz",
    "mnist/train-images-idx3-ubyte.gz",
              "mnist/svm-train-min.json", 1000, FALSE);
    convert_mnist(
    "mnist/t10k-labels-idx1-ubyte.gz",
    "mnist/t10k-images-idx3-ubyte.gz",
                  "mnist/svm-t10k-min.json", 1000, FALSE);
    // フルセットのデータを生成
    convert_mnist(
    "mnist/train-labels-idx1-ubyte.gz",
    "mnist/train-images-idx3-ubyte.gz",
                  "mnist/svm-train.json", 999999);
    convert_mnist(
    "mnist/t10k-labels-idx1-ubyte.gz",
    "mnist/t10k-images-idx3-ubyte.gz",
                  "mnist/svm-t10k.json", 999999);
    
    function convert_mnist($label_f, $image_f, $output, $limit = 999999, $cache = TRUE){
        if ($cache){
            if (file_exists(output)) return;
        }
        $label_f2 = str_replace('.gz', '', $label_f);
        $image_f2 = str_replace('.gz', '', $image_f);
        gz_uncomp($label_f, $label_f2);
        gz_uncomp($image_f, $image_f2);
    // ファイルを開く
        $label_fp = fopen($label_f2, "r");
        $image_fp = fopen($image_f2, "r");
        $out_fp = fopen($output, "w");
        // マジックナンバーをチェック
        $mag = fread_int32($label_fp);
        if($mag !== 0x0801) die("ファイル破損:$label_f");
        $mag = fread_int32($image_fp);
        if ($mag !== 0x0803) die("ファイル破損:$image_f");
        // アイテム数を確認
        $num_items = fread_int32($label_fp);
        $num_items = fread_int32($image_fp);
        echo "アイテム数: $num_items\n";
        // 行数のピクセル数
        $num_rows = fread_int32($image_fp);
        $num_cols = fread_int32($image_fp);
        $num_pix = $num_rows * $num_cols;
        echo "行列: $num_cols x $num_rows\n";
        if ($limit < $num_items) $num_items = $limit;
        fwrite($out_fp, "&#91;");
        
        // 各データを読みだして出力
        for ($i = 0; $i < $num_items; $i++){
            $label = $lno = fread_b($label_fp);
            $images = &#91;&#93;; $values = &#91;&#93;;
            for ($j = 0; $j < $num_pix; $j++){
                $images&#91;&#93; = $v = fread_b($image_fp);
                //if ($v == 0) continue;
                $values&#91;$j&#93; = $v / 255;
            }
            // 本当に取り出せたかPGMに保存してテスト
            if ($i < 10){
                $s = "P2 28 28 255\n";
                $s .= implode(" ", $images);
                file_put_contents("mnist/test-$i-$lno.pgm", $s);
            }
            if ($i % 1000 == 0){
                echo "&#91;$i/$num_items&#93; $label - ".implode(",", $images)."\n";
            }
            // 書き込む
            fwrite($out_fp,
                   "&#91;".label.",".implode(",", $values)."&#93;");
            if($i > $limit) break;
            if($i == ($num_items-1)){
                fwrite($out_fp, "\n");
            } else {
                fwrite($out_fp, ",\n");
            }
        }
        fwrite($out_fp, "]\n");
        echo "[ok] $output\n";
    }
    
    function fread_int32($fp){
        $c4 = fread($fp, 4);
        return array_shift(unpack("N", $c4));
    }
    function fread_b($fp){
        $c = fread($fp, 1);
        return ord($c);
    }
    function gz_uncomp($in, $out){
        $raw = gzdecode(file_get_content($in));
        file_put_contents($out, $raw);
    }

オイーブベイズ分類

<?php
// ライブラリの取り込み
    require_once 'vendor/autoload.php';
    require_once 'MecabTokenizer.php';
    use Fieq\Bayes\Classifier;

// 単語分割器と分類器の生成
    $tokenizer = new MecabTokenizer();
    $classifier = new Classifier($tokenizer);

// 単語を学習させる
    $classifier->train('迷惑', 'とにかく安いので一度見に来てね。');
    $classifier->train('迷惑', '驚異のリピート率。高額時給のバイトを紹介します。');
    $classifier->train('迷惑', 'ついに解禁。神秘の薬。あなただけに教えます。');
    $classifier->train('重要', '返却予定日のお知らせ。');
    $classifier->train('重要', 'いつもお世話にしています。予定の確認です。');
    $classifier->train('重要', 'オークション落札のお知らせです。');
    
// 分類してみる。
    $s = 'プロジェクトAの件で予定を確認させてください。';
    $r1 = $classifier->classify($s);
    echo "--- $s\n";
    print_r($r1);

    
    $s = 'とにかく安さが自慢のお店です。見てください。';
    $r2 = $classifier->classify($s);
    echo "--- $s\n";
    print_r($r2);