技術的な話題 に関する投稿を表示しています

MySQL の文字コードについてチョット調べた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

MySQL 使ってて utf8_unicode_ci や utf8_general_ci とか mb4 …? とプチ事故が起きたので解消するべく調べた。

照合順序とは

「utf8_general_ci」といった文字コードの指定してるっぽいやつは、文字列カラムにおけるコレーション(=照合順序)と呼ばれる。ここで指定された値によって、MySQL が文字列を解釈したり比較を行うようになっている。

表記は 文字コード_文字セット名_比較方法(※特殊なものとして *_bin もある)

  • 文字コード
    • utf8mb4 とか utf8 とか cp932 とか
  • 言文字セット名
    • japanese とか german とか
    • Unicodeに限り、japanese はないので general か unicode か bin を選ぶ。
      • (言語によって文字の特殊性があるらしく、その言語別の定義があるが日本語はない)
  • 比較方法
    • 3種類あある
    • ci
      • 大文字小文字を比較しない
    • cs
      • 大文字小文字を比較する
    • bin
      • バイナリとして判断する
    • よく使う utf8... では ci と bin のみになる…?(何でー?)

どんな照合順序が使えるかを確認する

show collation というクエリを実行することで確認できる。

show collation;
Collation Charset Id Default Compiled Sortlen
big5_chinese_ci big5 1 Yes Yes 1
big5_bin big5 84 Yes 1
dec8_swedish_ci 8-Dec 3 Yes Yes 1
dec8_bin 8-Dec 69 Yes 1
cp850_general_ci cp850 4 Yes Yes 1
cp850_bin cp850 80 Yes 1
hp8_english_ci hp8 6 Yes Yes 1
hp8_bin hp8 72 Yes 1
koi8r_general_ci koi8r 7 Yes Yes 1
koi8r_bin koi8r 74 Yes 1
latin1_german1_ci latin1 5 Yes 1
latin1_swedish_ci latin1 8 Yes Yes 1
latin1_danish_ci latin1 15 Yes 1
...

※ like など SQL を書くときに使うような演算子を利用して絞り込みができたり、 ORDER BY 並び順を設定できる。

show collation where Collation like 'utf8%';
Collation Charset Id Default Compiled Sortlen
utf8_general_ci utf8 33 Yes Yes 1
utf8_bin utf8 83 Yes 1
utf8_unicode_ci utf8 192 Yes 8
utf8_icelandic_ci utf8 193 Yes 8

MySQL サーバーのバージョンによって使える照合順序が違うので、環境によって結果が変わることがある。(古いと utf8mb4 がないとかそういう) 5.6, 5.7 あたりならこの記事の話は問題ないはず。詳しい一覧はここで確認ができる。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.14 MySQL でサポートされる文字セットと照合順序

*_unicode_ci と *_general_ci の違い

照合順序の違いによってどんなことが起こるか、実際にクエリを発行しつつ確認する。

# バージョン確認
select version();
# 5.7.17


# アルファベットの大文字小文字、半角全角
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 1, 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 0, 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 0, 0, 0
 
 
# ひらがな, 半角カタカナ + 半濁音
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'は' = 'パ';
# 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'は' = 'パ';
# 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'は' = 'パ';
# 0

この結果をまとめると。

半角全角の比較 大文字小文字の比較 ひらがなカタカナの比較
unicode_ci できない できない できない
general_ci できる できない できる
bin できる できる できる

というわけで general_ci か bin を使うようにするとこのあたりの比較がちゃんと出来てよさそう。

utf8 と utf8mb4 の違い

MySQL の UTF-8 は 3 バイトの幅しかなく、絵文字などを表現するためには 4 バイトが必要。きっと歴史的な背景から utf8 と utf8mb4 が分裂している。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.10.7 utf8mb4 文字セット (4 バイトの UTF-8 Unicode エンコーディング)

先程と同じように文字コードを指定して比較していく。

SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_bin';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8' COLLATE 'utf8_unicode_520_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_520_ci';
select 'あ' = '?', '?' = '?';
# 0, 0

// あれ、このあたり文字化けしてるかも…? 
// あ=絵文字1, 絵文字1=絵文字2; と絵文字を比較してます。。

むむ、結構厄介。絵文字が入力される可能性があり、ちゃんと識別したい場合は utfmb4_bin を使うのがよさそう。

参考リンク

utf8_unicode_ci に対する日本の開発者の見解 - かみぽわーる
MySQL と寿司ビール問題 - かみぽわーる

Laravel5 で Log ファサードを使ったときに一緒に標準出力にも出した

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

artisan コマンドを作ったとき、何かと様子を出力するために Log ファサードを使ってログファイルに出している。

 

ちなみに。そのとき Monolog のハンドラを使って、レベル別にファイルを分けたりなどしている。こんな具合にするとログレベルごとにファイルが分かれる、めっちゃ便利。

// bootstrap/app.php
$app->configureMonologUsing(function ($monolog) {
    $logConfigure = config('log');
    foreach ($logConfigure['file'] as $logLevel => $logFile) {
        $path = storage_path('logs/' . $logFile . '.log');
        $handler = new \Monolog\Handler\RotatingFileHandler(
            $path,
            $logConfigure['rotate'],
            $logLevel,
            false,
            $logConfigure['permission']
        );
        $handler->setFormatter(new Monolog\Formatter\JsonFormatter());
        $monolog->pushHandler($handler);
    }
});


// config/log
return [
    'permission' => 0777,
    'rotate' => 30,
    'file' => [
        \Monolog\Logger::DEBUG     => 'debug',
        \Monolog\Logger::INFO      => 'debug',
        \Monolog\Logger::NOTICE    => 'debug',
        \Monolog\Logger::WARNING   => 'warning',
        \Monolog\Logger::ERROR     => 'error',
        \Monolog\Logger::CRITICAL  => 'error',
        \Monolog\Logger::ALERT     => 'error',
        \Monolog\Logger::EMERGENCY => 'error',
    ],
];

自分で用意する artisan コマンド、ようはバッチでは様子の出力などをファイルに出すために Log::info(); とかして、いろいろな情報をログに出ていく。この ID のデータをやるよー、とか、おわったよー、とか、色々。

しかし開発中はそれって厄介で、パッとコマンド実行してログ見て。いや、横で tail -f とかしておけば良いのだろうけど、それはそれでちょっと面倒だなあ。というか artisan コマンドって verbose なオプションをサポートしているので、それをうまく使ってログに出すと同時に標準出力にも出せないだろうか。

と思って出来たのがコレ。

namespace App\Console;

use Illuminate\Console\Command;
use Monolog\Handler\StreamHandler;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

trait LogWithStdout
{
    /**
     * execute メソッドに割り込み Monolog の設定をする
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        // verboseなオプションが付いたらログを画面に出す
        if ($output->getVerbosity() >= $this->verbosityMap['v']) {
            $monolog = \Log::getMonolog();
            $handler = new StreamHandler("php://stdout", 'info');
            $monolog->pushHandler($handler);
        }

        // 元の処理の呼び出し
        return parent::execute($input, $output);
    }
}
namespace App\Console\Commands;

use App\Console\LogWithStdout;
use Illuminate\Console\Command;

class FetchTrafficSummary extends Command
{
    use LogWithStdout;

    public function handle()
    {
        Log::info('start!');

        ...

        Log::info('complete!');
    }
}

本当はサービスプロバイダにしてイベントにしてあげたほうがキレイなのだが、 Laravel の実装上、それは出来ないっぽい…。うぬ。。

具体的にはコマンドの発火時は取れるのだが verbose オプションがあるかどうかが取れない。 OutputInterface がイベントに入ってこないのだ。ソースも追いかけてみたがイベント引数に入ってくるのは Application インスタンスだけ。 OutputInterface を持ったものは Application から Command を呼ぶときにしか作られないらしい。

 
 
とまあ、作ってみたものの Artisan コマンドでも $this->info などして標準出力に出せるので、そちらをオーバーラップしたほうがキレイなんじゃないかなーと思った。こういうイメージ。

public function info($string, $verbosity = null)
{
    parent::info($string, $verbosity);
    \Log::info($string);
}

どちらかといえばこちらのほうが、メソッド別にログファサードのメソッドを呼べるのでパッと見でわかりやすそうだ。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

TF-IDF は文章における、単語の重みづけを行うもの。
処理した文章中のうち、ある単語はどの程度重要なものか、出現度合いから重み付けを計算する。
プログラム、機械からは文章の分析っていってもよくわからないので、そのよくわからない特徴をわかるようにするための、数値化する方法の1つ。

前提として TF-IDF には複数の文章を入力する必要がある。

TF-IDF = TF * IDF

TF = 1つの文章において、ある単語の出現回数 / 文章内の単語数
→ 文章1つずつの単語の出現頻度がわかる。

IDF = log( 文章数 / ある単語が出現する文章数 ) + 1
→ 横断的に使われる単語は低い値になる。

簡単な文章を入れつつ実際の数字を計算してみる。

 
step1:文章の入力

文章
リンゴとレモンとレモン
リンゴとミカン

 
step2:単語に分割(ここでは形態素解析し、名詞だけに絞ったとする)

文章 分割した結果
リンゴとレモンとレモン リンゴ レモン レモン
リンゴとミカン リンゴ ミカン

 
step3:TF値、IDF値を計算する

文章 TF:リンゴ TF:レモン TF:ミカン
リンゴとレモンとレモン 1 / 3 = 0.33 2 / 3 = 0.67 0 / 3 = 0
リンゴとミカン 1 / 2 = 0.5 0 / 2 = 0 1 / 2 = 0.5
単語 IDF
リンゴ log(2 / 2) + 1 = 1
レモン log(2 / 1) + 1 = 1.3
ミカン log(2 / 1) + 1 = 1.3

 
step4:TF-IDFを計算する

文章 TF-IDF:リンゴ TF-IDF:レモン TF-IDF:ミカン
リンゴとレモンとレモン 0.33 * 1 = 0.33 0.67 * 1.3 = 0.87 0 * 1.3 = 0
リンゴとミカン 0.5 * 1 = 0.5 0 * 1.3 = 0 0.5 * 1.3 = 0.65

 

この結果からわかることとして。

  • 「リンゴ」は横断的に出現するので値が低くなる。
  • 「レモン」は文章1だけに多く使われるので値に特徴が強く出る。
  • 「ミカン」は文章2だけに使われているが、リンゴと同程度しか使われていないので、特徴が大きく出るわけではない。
  • この2つの文章を切り分けるには レモン の様子を見ればよいらしい。ということで、確かに単語の重要度がわかるよ、っていうのはあっているらしい。

ただ、全ての文章・全ての単語を利用するという計算ロジックの都合、文章を逐次増やして差分計算していくー、というのは出来なさそうなので、そういう用途はむりそうだなー。
例えばこの計算済み TF-IDF の結果に加えて「バナナとレモン」を追加したときに困っちゃうね、という話。この量なら良いけど、もっと文章量が増えてもっと単語量が増えると、毎回計算し直すコストがかかる。

Composer で指定したライブラリがインストールできないんだけど!!と言われたと

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

composer require コマンドでライブラリを入れようとすると「ライブラリをいれたいけど入れられないよ!」なんて怒られることがある。いや、 composer install でも起こることあるわ。

起きたこと

※既に他のライブラリを入っている状態

$ composer require --dev "phpdocumentor/phpdocumentor:^2.8"
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
./composer.json has been updated

Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.
 
  Problem 1
    - phpdocumentor/phpdocumentor v2.8.0 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.1 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.2 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.3 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.4 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.5 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - Conclusion: don't install herrera-io/phar-update 1.0.3
    - Installation request for phpdocumentor/phpdocumentor ^2.8 -> satisfiable by phpdocumentor/phpdocumentor[v2.8.0, v2.8.1, v2.8.2, v2.8.3, v2.8.4, v2.8.5].
 
 
Installation failed, reverting ./composer.json to its original content.

原因

Composer は依存ライブラリの解決を行う際、それぞれのライブラリの composer.json に記載されているバージョンを解釈し、お互いに問題のない、なるべく最新のものを選択しようとします。

このとき「あるライブラリではAの1.0.* を使います。別のあるライブラリではAの2.0.*を使います」といった記述がされていると、エラーになってしまうようです。

あるいは composer.lock としてバージョンが固定されてしまうときにも同様の問題が起こります。依存を再解決しようとするときに上記のようにぶつかってしまうことがあるようです。

対応策

順番に確認していくと良さそう。

  1. そもそも composer.json で書かれているバージョンとエラーになっているバージョンとが噛み合わない
    • → 使っているライブラリ、または入れようとしているライブラリのバージョンを見直す
  2. そんなことはない、依存していたんだ、というくらいのものでエラーが出てる
    • → composer.lock を消して composer install

どちらにしても、依存ライブラリのバージョンが変わるはずなので、テストの実行や動作確認を行ったほうがよいです。

Composer は何者か。あるいは install と update の違い。そしてオートロードの仕組み。

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

たいとるそのまま。

Composer 使って Packagist から ライブラリを入れられるけど結局何者なんだ、 install と update の違いはなんだ、つーかオートロードってどうなってんねん、などなどがよくわからなかったのでドキュメントとかソースを見た。

 

Composer とは?

Introduction - Composer

このあたりを、見ると大体解決する。

Composer "Dependency Manager for PHP"
→ PHP向けの依存関係管理システム

Ruby における Bundler や Node.js における npm のように、 PHP に対しての Composer 。

Composer 自体も PHP で書かれていて、PHP 5.3.2 以降の環境ならうまく動いてくれる。
場合によっては、コンパイルオプションの指定やモジュールの追加、コンフィグの問題などが発生するが、 Composer が判別し、警告を出してくれるのでその時に対応すれば良い。

 

依存関係管理?

作っていく PHP アプリケーション(あるいは PHP ライブラリ)が、ほかのライブラリに「依存」している場合、依存しているライブラリもセットにしないと動作しない。

仮にそのライブラリがまた別のライブラリに「依存」していたら…?
あるいは、ライブラリ A と別のライブラリ B が、ある同じライブラリ C に依存しているけど、 C と C' のように違うバージョンが対象だったら…?

などなどの非常にやっかいなライブラリ依存関係を、簡単な記述で管理できるようにしたものが依存関係管理システム。

PHP なら PEAR がある。が、今この時代では Composer のほうがよく使われている…はず。

 

Composer インストール

composerのインストールは非常に簡単。

$ curl -sS https://getcomposer.org/installer | php

この場合はカレントディレクトリに comopser.phar というファイルが作られ、これがComposerの本体になる。

どのディレクトリからでも実行できるように、どのユーザでも実行できるようにするためにはシステムにインストールする必要がある。

その際には、以下のコマンドで。

$ sudo mv composer.phar /usr/local/bin/composer
$ sudo chown root:root /usr/local/bin/composer
$ sudo chmod +x /usr/local/bin/composer

 

Composer がすること

  • composer コマンドの提供
  • 依存するライブラリについて
  • 依存関係の解決(ダウンロード&展開)
  • ライブラリ検索
  • 依存関係の追加
  • 依存関係の除去
  • オートロード用ファイルの提供

 

composer コマンドの提供

Composer をインストールすると composer というコマンドが利用できる。

※リネームしていない場合 composer.phar が実行コマンド。
実行権限が無い場合は php composer.phar XXXX のように php コマンドへの入力として実行する必要あり。

composer コマンドには、おおざっぱに以下の機能がある。

  • init
    • カレントディレクトに新しく composer.json を生成する
  • require
    • 依存関係を追加する
  • remove
    • 依存関係を削除する
  • install
    • composer.json に基づいて依存関係を解決する
    • composer.lock というファイルが存在する場合は、それに従う。
  • update
    • composer.json に基づいて依存関係を再度解決し composer.lock ファイルを更新する
  • dump-autoload
    • オートロード用ファイルを生成する
  • search
    • ライブラリを Packagist から探す
  • self-update
    • composer コマンド自体を更新する

他にも細かいところがあるので、気になったらドキュメントへ。
Command-line interface / Commands - Composer

 

composer install と composer update の違い

install

  • 7文字
  • composer.lock があればそれに従ってライブラリを導入する
  • composer.json に従ってライブラリを導入する

update

  • 6文字
  • composer.lock があっても composer.json に従ってライブラリを導入する

composer.lock は実際にインストールされているライブラリ、バージョンが記載されている。
composer.json には、このアプリケーション(ライブラリ)が一体どのライブラリ、どのバージョンに依存しているかを記載する。そのときライブラリのバージョンアップを形容できる書き方がある。

 

composer.json におけるバージョン指定の方法

詳しくはドキュメントにまとまってる。
Versions and constraints - Composer

ざっくりまとめると。

  • 固定値
    • 1.0.2
  • 範囲指定
    • > >= < <= != ||
    • 記号は書いたそのままの意味、|| は OR 。スペースで AND 。
    • >=1.0
      • 1.0 以上
    • >=1.0 <2.0
      • 1.0 以上 かつ 2.0 未満
    • >=1.0 <1.1 || >=1.2
      • (1.0 以上 かつ 1.1 未満)または 1.2 以上
  • ハイフンの範囲指定
    • 1.0 - 2.0
      • >=1.0.0 <2.1 と等価
      • 1.0 以上 かつ 2.1 未満
  • ワイルドカード
    • 1.0.*
      • >= 1.0 <1.1
      • 1.0 以上 かつ 1.1 未満
  • チルダ
    • 指定した一番小さいバージョンの変化を形容する
    • ~1.2
      • >= 1.2 <2.0
    • ~1.2.3
      • >= 1.2.3 <1.3
  • ハット
    • セマンティックバージョニングに従う = メジャーバージョンを上げない
    • ただし 1.0 未満はマイナーバージョンに合わせる
    • ^1.2.3
      • >=1.2.3 <2.0.0
    • ^0.3
      • >=0.3.0 <0.4.0

チルダ + マイナーバージョン指定にしておけば、大きな変更に殺されることはなさそう。

 

Composer の提供するオートローダーとは

composer dump-autoload を実行すると、 composer.json の内容にもとづいてオートロード用のファイルが作られる。ファイルは複数あり、以下の場所で確認できる。

  • vendor/autoload.php
  • vendor/composer/*

これらのファイルは次のような仕組みで成り立っている。ソースにコメントをつけて流れを見ていく。

<?php
// vendor/composer/autoload_real.php を読み込み、そちらに処理が移る。

require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d::getLoader();

vendor/composer/autoload_real.php

<?php
// 余談だけどこのハッシュ値っぽいものは指定できる。
// composoer config に autoloader-suffix があればそれで、なければ md5(uniqid('', true)) が使われる。
//
// composer.json に書くなら config > autoloader-suffix になるようにする。
// composer config autoloader-suffix HogeeeHugaaa でも可。
class ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d
{
    // ClassLoader インスタンス
    private static $loader;

    // spl_autoload_register によって利用される ClassLoader を読み込む
    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        // ClassLoader の読み込み
        spl_autoload_register(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'));

        // PHP で、かつ Zend Guard Loader の拡張が有効なら autoload_static.php を利用する
        // 余談)Zned Guard Loader は Zned Optimizer によって作られたエンコードされた PHP ファイルを実行するためのもの
        // http://www.zend.com/en/products/guard/zend-optimizer-zend-loader
        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitaf2b1a382e2c643bceff8e5e7a5cce1d::getInitializer($loader));
        } else {
            // 条件に該当しなかった場合は、各ファイルを個別に読み込む

            // 名前空間の定義がされたものを読み込み
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            // PSR-4 形式で定義されたものを読み込み
            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            // 直にクラスが指定されているものを読み込み
            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        // 読み込んだファイル群を spl_autoload_register に登録する
        $loader->register(true);

        return $loader;
    }
}

vendor/composer/ClassLoader.php (抜粋)

// spl_autoload_register を使って登録する
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}


// spl_autoload_register によって呼ばれるメソッド
// ファイルが見つかれば include する
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}


// オードロード要求のクラスが含まれるファイルを探す
public function findFile($class)
{
    // classmap ファイルから読み込んだリストにいれば、それを返す
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }

    // 「ファイルが見つからなかった」がキャッシュされていれば当然見つからない
    // あるいは dump-autoload のときに classmap-authoritative が指定されていれば classmap だけにする
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // APCu による最適化が有効になっていれば APCu から読み込めるか試みる
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

    // クラス名 .php のファイルを探す
    $file = $this->findFileWithExtension($class, '.php');

    // HHVM で動いているなら .hh ファイルを探す
    if (false === $file && defined('HHVM_VERSION')) {
        $file = $this->findFileWithExtension($class, '.hh');
    }

    // APCu 最適化が有効なら APCu にキャッシュ登録する
    if (null !== $this->apcuPrefix) {
        apcu_add($this->apcuPrefix.$class, $file);
    }

    // 「ファイルが見つからなかった」のキャッシュ
    if (false === $file) {
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}


// クラス名 + 拡張子 のファイルを探すメソッド
private function findFileWithExtension($class, $ext)
{
    // PSR-4 形式のファイルを探す
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath.'\\';
            if (isset($this->prefixDirsPsr4[$search])) {
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    $length = $this->prefixLengthsPsr4[$first][$search];
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-4 のフォールバック
    // ※ 名前空間 => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // PSR-0 形式のファイルを探す
    if (false !== $pos = strrpos($class, '\\')) {
        // namespaced class name
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // PSR-0 は PEAR 形式のファイル名も形容するので、それを探す
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    if (isset($this->prefixesPsr0[$first])) {
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
            if (0 === strpos($class, $prefix)) {
                foreach ($dirs as $dir) {
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-0 のフォールバック
    // ※ プリフィックス => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // PSR-0 のインクルードパスの解決
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    // 見つからねーもんは見つからない。
    return false;
}


// 指定したファイルを include する
// ※これだけグローバル空間にいる
// ※includeしたものから $this や self を利用できないように、のため
function includeFile($file)
{
    include $file;
}

で、autoload_real から 読み込まれるファイルがいくつかあるけどなんやねん、という話は composer.json に書く。書き方はドキュメントに詳しくある。

The composer.json Schema - Composer

ざっくりとまとめると。

  • PSR-4
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
  • PSR-0
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
    • PSR-4 との違いは アンダースコアでも切ることができる、名前空間なしファイルが存在すること
  • classmap
    • 名前空間など PSR-0 または PSR-4 に従っていないもの
    • クラスのあるディレクトリやファイルを直に指定する
  • file
    • 細かいこととかいいから、俺はこのファイルを読み込みたいんだ

こんな感じ。 PSR-0 と PSR-4 はざっくりだけど、詳しく書くともっと長くなるのでまた。

 

オートローダーの最適化

このあたりのざっくりまとめ。

Autoloader Optimization - Composer

特にオプションなしに composer install または composer update 。 あるいは composer dump-autoload を実行すると、上記のまま処理が動く。

多数のライブラリを読み込むような巨大なフレームワーク、アプリケーションなどは、特に PSR-0 や PSR-4 のディレクトリ構成を解析して読み込む件で時間がかかってしまう。そこで、事前にオートローダーを最適化することができる。 -o オプションをつけるとよい。

$ composer dump-autoload -o

これで PSR-0 も PSR-4 も全てのオードロードの記述が classmap に変換される。 classmap になることで、クラス名からディレクトリを探しにいくことがなくなりその分高速化が見込める。

 
さらに本番環境など、ソースがもりもり変わることがなければ -a というオプションも付けるとよい。上のソース解説で出てきた classmap-authoritative のくだり。

$ composer dump-autoload -o -a

このオプションがあれば classmap に見つからないときに処理を諦める。そもそも composer dump-autoload なしに本番のソースが変わることはないだろうので、つけたほうが良い。が、可能性として、クラスが見つかりません!になってしまうことがある。

 
何かしらの都合で -a オプションはだめだー!となるなら APCu を有効にする戦略も取れる。 -a に比べて遅くはなるが、クラスが見つかりません!問題に対応しやすくなる。とはいえ APCu はメモリに乗っかってくるので、メモリに余裕があるような環境じゃないと使えない。

$ composer dump-autoload -o -apcu

上のソースを見てわかるとおり classmap-authoritative と APCu は同居しない、どちらかしか使えない。


複雑に絡み合ったひもがちょっとずつ解けていくように Composer の動きに詳しくなっていく気持ちがする。

が、ちょっとつかれたのでここでおちまい。

算術平均を逐次計算したい。

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

数式をこねくりまわす知識や技術はないので自分で求められないのですが、平均値って逐次計算できるのかな?を調べたメモ。

x = [...] // 元の数列
z = ...   // 追加したい数

// 平均値
Average(x) = Sum(x) / Count(x)

// 数を追加した平均値
Average(x + z) = Average(x) + ((z - Average(x)) / Count(x))

調べていたらこういう式も。平均に個数をかけて数を足し、そこから新しい個数で割れば、新しい平均だよね、というもの。そりゃそうだわ。

Average(x + z) = (Average(x) * Count(x) + z) / Count(x + z)

どちらにしても個数と平均値だけ持っておけば逐次計算できますね。後者は掛け算するので、個数や平均値が大きすぎると扱えなさそうなので、前者のほうがよさそう。

試しに実装してみるとこういう形。

class MeanStream
{
    private $mean = 0;
    private $dataCount = 0;

    public function __construct() {
    }

    public function add($num) {
        $this->dataCount++;
        $this->mean = $this->mean + (($num - $this->mean) / $this->dataCount);
        return $this;
    }

    public function get() {
        return $this->mean;
    }
}

echo new MeanStream()
    ->add(1)
    ->add(7)
    ->add(8)
    ->add(6)
    ->add(2)
    ->add(5)
    ->add(1)
    ->add(8)
    ->add(9)
    ->add(4)
    ->get();

echo (1+7+8+6+2+5+1+8+9+4) / 10;
// 5.1

あってますねー

 
ちなみに、当然のことながら上記の例だと浮動小数点数を使うので、使う値によっては表現の都合で微妙に数値が変わるのでその点だけ注意ですね。

$values = [];
for($i = 0; $i < 10; $i++) {
    $values[] = mt_rand();
}

$mean = new MeanStream();
foreach ($values as $value) {
    $mean->add($value);
}

echo sprintf('%.30f', array_sum($values) / count($values));
// 995988838.500000000000000000000000000000

echo sprintf('%.30f', $mean->get());
// 995988838.499999880790710449218750000000

PHP じゃない言語で実装するとして、もし Decimal のような、よろしく精度をもってくれる機能があればそれを使ってあげるほうがよさそうです。PHP は言語として標準に Decimal がないのでどうするんだろう、 bcmath とか gmp というのがありそうだけど extension に左右されるなー。まあ composer.json で extension について依存あるよーと記載すればいいのか。
PHP: 数学 - Manual

Packagist みると幾つかありそうなので、このあたりを使ったほうがいいのかなー。

 
と、話がそれてきたあたりでおしまい。ライブラリの話はまた今度。

Atom と Hydrogen で Jupyter が Atom で動くよ!(インストール、準備、使い方まで)

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

一時期 Atom を使ってたけど、もっさり感がどうにもならないのと、簡易的なメモ(新規ファイル作って保存せずメモしておきたい、エディタ再起動しても残っていてほしい)が残せなかったっぽいのもあって、しばらくずっと PhpStorm か SublimeText を使っています。

最近になって、普段使いの PC を新しいものに変更してスペックも上がったし Atom もいろいろバージョンアップしているようだしどうだろうなあ、と思って Atom のプラグインを色々見てたら Jupyter を触れるとかなんとか書いてあるではありませんか!
謎技術だなあと思いつつもエディタ上で Jupyter のように振る舞ってくれるならそれはとっても素敵だなーと思ったので、試してみたの巻。

 

Atom + Jupyter をやるには?

Hydrogen というパッケージを使うと Atom 上で Jupyter かのように一行ずつ実行したり、結果をグラフィカルに表示したりが出来ます。

内部的にはインストールされている Python で Jupyter を起動。 Jupyter が用意しているメッセージプロトコルを利用して nodejs と通信し、その結果を Atom で見えるように整形している、というものです。

参照)nteract/hydrogen: Run code inline in Atom using Jupyter kernels
※ How it works のくだり。

この Hydrogen を Atom にインストールしてやります。
それでは、順番に環境を準備していきましょう。

Atom

とくに言うことはないです。公式サイトからどうぞ。

Atom

Python の準備

こちらも同様に公式サイトからどうぞ。バージョンは 3 系で良いです。執筆時時点なら 3.6 あたりで良いと思います。
もし Windows を使っているなら Windows 用インストーラを選ぶとよいでしょう。

Welcome to Python.org

Jupyter の準備

少々雑ですが、いろいろ pip install したら良いです。

pip install -U pip

pip install ipython
pip install jupyter
pip install matplotlib

// 試しに利用したい //
pip install numpy
pip install scipy
pip install pandas
pip install scikit-learn

※ Windowsの場合
主に numpy と scipy と scikit-learn あたり(多分)でコンパイルエラーになってしまうので、別途配布しているサイトから whl ファイルをダウンロードして、それを使って登録する必要があります。

Python Extension Packages for Windows - Christoph Gohlke

pip install <ダウンロードした whl ファイル>

Hydrogen を入れる

Atom 内のパッケージ管理からインストールできます。Hydrogen で検索すれば一発でしょう。


(既にインストール済みなのでアンインストールボタンですが、通常はインストールボタンになります)

Atom + Hydrogen を試してみる

これ以上の準備は不要です。 Atom 上で Python を書いて、おもむろに Ctrl + Alt + Enter を押すと、勝手に Jupyter が裏で起動し、ソースを実行してくれます(スゴイ)
一括実行やブロック実行もできるので Jupyter がそのまんま Atom 上で動いているようにも見えます。

試しに numpy でランダムなデータを作成し KMeans で 5 つに色分けしてみた結果です。


 
キャプチャの通り Windows ですが scikit-learn も動きますし matplotlib の結果もちゃんと Atom 内で表示してくれます、スゴイ!!

え?Tensorflow?Chainer?
動くんじゃないですか? Atom で Python が動いている!!!ように見えるだけで、裏側は結局 Python + Jupyter でしかないので、それが動く環境なら動作するはずです。

Hydrogen の使い方

Jupyter ではそれぞれの Cell が明確に入力欄が別れていたり、なんとなく Ctrl + Enter を押したり、実行できそうなボタンを押すなどして動かしていましたが、 Hydrogen ではただの Atom エディタになるので明確に Cell の境界が見えることはありませんしどうなっているのでしょう。

公式ドキュメントにかかれていることとほぼおなじなので、そちらを見ている場合はこっちは見なくても良いです。
Getting Started · Hydrogen

コードの実行

デフォルトでは以下のようなキーバインドがされています。

キー コマンド 処理内容
ctrl-enter hydrogen:run 選択されているコードのみを実行する
ctrl-alt-enter hydrogen:run-cell 現在のセルを実行する
alt-shift-enter hydrogen:run-cell-and-move-down 今のセルを実行し、次のセルに移る
ctrl-alt-shift-enter hydrogen:run-all 全てのセルを実行する

セルを分けるには Jupyter と同じ記述の # In[ ]: という形を使うのが良さそうです。

# In[]:
x = 10
y = 10
(x, y)  # 10, 10

# In[]:
z = x * y
z  # 100

実際に試してる様子はこんな感じ。左からコード実行、セル実行、全実行。

カーネルの再起動

これはコマンドパレットから。なんか挙動がおかしいぞ?って時によく使いますよね。使わない…?

gif 見ての通り、再起動しても出力結果は消えないので、そこも消したいんだけど!!!という場合には Clear Result を別途実行する必要があります。

出力エリアのトグル

デフォルトでは、コードのすぐ下に出力されます。これはこれで便利ではあると思うのですが、コード量が増えてくるとうっとうしく感じることもあるかもしれません。 Hydrogen には出力エリアが準備されていて、それを開くことで、そちらに全て出るようになります。
しかもページ分けされて便利!

変数ウォッチ機能

これ Jupyter にありましたっけ…?あったら知らない私がごめんなさいなんですけど。。ともあれ、この機能めっちゃすごくないですか、変数を変えていくとリアルタイムで出力結果を確認できるんですよ…、どうなってんねん…。

クラス、関数のインスペクション

ライブラリだったり、自分で実装したクラスや関数について、説明を見ることが出来ます。公式ドキュメント片手にコードを書くことが多い私的には便利に使えそうです。

コード補完

ライブラリの中、変数の中に結局こういうメソッドってあったっけ?をドキュメントに見に行ったり私には非常に嬉しい機能です。
注意点としては、すぐ補完が効くわけではなく、その変数やクラスがカーネル側に認識されていないといけません。つまり、一度そのコードを実行して、認識させる必要があります。以下の gif では Ctrl+Enter を押して pred をカーネル側に認識させています。

おわりに

というわけで Atom + Hydrogen で Jupyter Notebook が使えるよ、って話でした。

Jupyter それ自体は非常に便利ですが、普段使うエディタを勝手が異なったりすると、それはそれで使いにくいですよね。。ショートカットキーだとか、テーマだとか…。。
Atom 自体がそこそこ重たいので、スペック高めのPCでないと難しいかもしれませんが、 Python 用のエディタとして Atom がかなり有力な選択肢になるんじゃないでしょうか?

ぜひお試しあれ!

形態素解析した後に結合処理をしてあげると良いかもね

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

MeCab や Kuromoji などで行える形態素解析、すごいですよねー。ただ新しい語が増えたり、なんだりーってしたときに、辞書を更新しないとちょっとだけ直感的ではない分割のされ方になりますよね。まあ、辞書を元に分割の仕方を学習するのでそうなんですが。。
そこに対して mecab-ipadic-neologd という素晴らしいプロジェクトがあって、最近の語もどんどん追加してくれています。

neologd/mecab-ipadic-neologd: Neologism dictionary based on the language resources on the Web for mecab-ipadic

これを使うと Github のサンプルにもかいてありますが「中居正広のミになる図書館」が固有名詞として認識されたりします!(スゴイ)

$ echo "10日放送の「中居正広のミになる図書館」(テレビ朝日系)で、SMAPの中居正広が、篠原信一の過去の勘違いを明かす一幕があった。" | mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd
10日    名詞,固有名詞,一般,*,*,*,10日,トオカ,トオカ
放送    名詞,サ変接続,*,*,*,*,放送,ホウソウ,ホーソー
の      助詞,連体化,*,*,*,*,の,ノ,ノ
「      記号,括弧開,*,*,*,*,「,「,「
中居正広のミになる図書館        名詞,固有名詞,一般,*,*,*,中居正広のミになる図書館,ナカイマサヒロノミニナルトショカン,ナカイマサヒロノミニナルトショカン
」      記号,括弧閉,*,*,*,*,」,」,」
(      記号,括弧開,*,*,*,*,(,(,(
テレビ朝日      名詞,固有名詞,組織,*,*,*,テレビ朝日,テレビアサヒ,テレビアサヒ
系      名詞,接尾,一般,*,*,*,系,ケイ,ケイ
)      記号,括弧閉,*,*,*,*,),),)
で      助詞,格助詞,一般,*,*,*,で,デ,デ
、      記号,読点,*,*,*,*,、,、,、
SMAP    名詞,固有名詞,一般,*,*,*,SMAP,スマップ,スマップ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
中居正広        名詞,固有名詞,人名,*,*,*,中居正広,ナカイマサヒロ,ナカイマサヒロ
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
、      記号,読点,*,*,*,*,、,、,、
篠原信一        名詞,固有名詞,人名,*,*,*,篠原信一,シノハラシンイチ,シノハラシンイチ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
過去    名詞,副詞可能,*,*,*,*,過去,カコ,カコ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
勘違い  名詞,サ変接続,*,*,*,*,勘違い,カンチガイ,カンチガイ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
明かす  動詞,自立,*,*,五段・サ行,基本形,明かす,アカス,アカス
一幕    名詞,一般,*,*,*,*,一幕,ヒトマク,ヒトマク
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
あっ    動詞,自立,*,*,五段・ラ行,連用タ接続,ある,アッ,アッ
た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。      記号,句点,*,*,*,*,。,。,。
EOS

(サンプルそのまま)

もうこれを使ったら良いんじゃないかなあと思います。

が!やっぱり実際に様々な文言をいれていくと、そういう分け方になるのねーだったり、それはちょっといらないんだよなー、というパターンは発生します。なんていうんでしょうか、特徴語抽出とか、なんかそういうときにちょっと不便になるんですよきっと。
そんなころに、形態素解析した後に品詞を見て結合処理をすると良い、と風の噂で聞いたので試しました。

※ここからは都合によって mecab-ipadic を使っています。 neologd の結果ではないので注意。※

まずは結合処理無しのものを見てみましょう。

Target: 「3Dブロー方式」でスピード乾燥。のびる、ひろがる「ふとん乾燥アタッチメント」

[   ' BOS/EOS *',
    '「 記号 括弧開',
    '3 名詞 数',
    'D 名詞 一般',
    'ブロー 名詞 一般',
    '方式 名詞 一般',
    '」 記号 括弧閉',
    'で 助詞 格助詞',
    'スピード 名詞 一般',
    '乾燥 名詞 サ変接続',
    '。 記号 一般',
    ' BOS/EOS *',
    ' BOS/EOS *',
    'のびる 動詞 自立',
    '、 記号 一般',
    'ひろがる 動詞 自立',
    '「 記号 括弧開',
    'ふとん 動詞 自立',
    '乾燥 名詞 サ変接続',
    'アタッチメント 名詞 一般',
    '」 記号 括弧閉',
    ' BOS/EOS *']

短い文章ですが、「3」「D」と分かれていたり、「3」「D」「ブロー」「方式」はくっついていたほうがいいんじゃないかなあ、などなど。もうちょっと、なあ、という部分がありそうです(主観)
こういったものをはじめ、色々な文章をいれつつ、こっちのほうがいいなー、これはためだなー、などと、目視で結合処理を増やしていったロジックがこれです。
 

形態素解析の結果を1語ずつループし、以下の処理をしていく

  1. ()[]’" が出てきたら
    1. 次のループへ※
  2. 記号が出てきたら
    1. ただし、今が半角で構成されている、かつ、1つ前も半角であれば
      1. 1つ前に今の語を結合
      2. 次のループへ
    2. 次のループへ※
  3. 今が半角で構成されている、かつ、1つ前も半角であれば
    1. 1つ前に今の語を結合する
    2. 次のループへ
  4. 1つ前が接頭詞なら
    1. 1つ前に今の語を結合する
    2. 品詞を今のものに置き換える
    3. 次のループへ
  5. 今が接尾なら
    1. 1つ前に今の語を結合する
    2. 次のループへ
  6. 1つ前が名詞、かつ、形容動詞語幹なら
    1. 今が助詞、助動詞、副詞、非自立のいずれか
      1. 次のループへ※
    2. 新しい語として追加
    3. 次のループへ
  7. 今が非自立なら
    1. 1つ前に結合する
    2. 次のループへ
  8. 今が終助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  9. 1つ前が名詞、かつ、今が名詞なら
    1. 1つ前が接尾なら
      1. 新しい語として追加
      2. 次のループへ
    2. 今が数なら
      1. 新しい語として追加
      2. 次のループへ
    3. 1つ前に結合する
    4. 次のループへ
  10. 1つ前が動詞または助動詞、かつ、今が動詞または助動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  11. 1つ前が動詞または助動詞、かつ、今が助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  12. 1つ前が形容詞、かつ、今が助動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  13. 1つ前が助詞類接続、かつ、今が動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  14. 1つ前が助詞、かつ、今が助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  15. 今が接頭詞、あるいは、助詞類接続、あるいは、数なら
    1. 新しい語として追加
    2. 次のループへ
  16. 今が助詞、あるいは、助動詞、あるいは、非自立なら
    1. 次のループへ※
  17. 新しい語として追加
  18. 次のループへ

 

それではこのロジックで同じ文章を試してみましょう。

Target: 「3Dブロー方式」でスピード乾燥。のびる、ひろがる「ふとん乾燥アタッチメント」

[   '3Dブロー方式 名詞 数',
    'スピード乾燥 名詞 一般',
    'のびる 動詞 自立',
    'ひろがる 動詞 自立',
    'ふとん 動詞 自立',
    '乾燥アタッチメント 名詞 サ変接続']

結合したので、品詞や種類などはもう完全に意味をなしていませんが、先ほどよりも、なんだか自然な感じで単語が取れているのではないでしょうか?(主観)

というわけで結合するとよくなりそうだよ、って紹介でした。

sentencepiece ニューラルネット時代における新しトークナイザ

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

自然言語処理をするとき n-gram や形態素解析などの処理をし、文章から語にすることがよくあると思います。 n-gram はシンプルですが、分け具合、分け方によって情報量がなくなってしまうこともありますし、データ量も大きくなりがちです。形態素解析は簡単お手軽そうに見えますが、裏の処理はなかなかコストが高く、ちりもつもればなんとやら。

というところで、新しい選択肢の sentencepiece だそうです。

https://github.com/google/sentencepiece

私の知識や理解力の不足から雑な説明にはなってしまいますが…。sentencepiece が行うことは次のことです。

1. 文章をよろしくニューラルネットワークで処理し語に分割するためのモデルを生成
2. そのモデルを使って文章を分割する
3. n-gram や 形態素解析のような何かが得られる!やったー!

この何かで機械翻訳をすると n-gram や形態素解析などと比べて劣ることなく十分に効果を発揮した、との記録もあります。

https://github.com/google/sentencepiece#results-bleu-scores

学習する、という初期コストがかかることについては、形態素解析も同じですが、その後の分割することについては、形態素解析よりもコスト低く行うことができます。また、語彙も形態素解析で扱う辞書に比べて遥かに小さくできるそうで、パフォーマンスよし、効果よし、ととても魅力的に見えますね!

実際に収録されているサンプルを試してみました。

環境構築は docker でお手軽にやってしまいます。

$ docker run -it -d centos:latest
$ docker ps

$ docker attach ...

コンテナの中で必要なツールの準備を進めていきます。

# yum install git make gcc-c++ autoconf automake libtool protobuf protobuf-devel
# git clone https://github.com/google/sentencepiece.git
# cd sentencepiece
# ./autogen.sh
# ./configure
# make

ここまでで実行ファイルが作られます。それでは文章から学習をしてみます。
サンプルとして吾輩は猫である...が収録されているので、これを使ってみます。

# src/spm_train --input=data/wagahaiwa_nekodearu.txt --model_prefix=neko --vocab_size=8000 --model_type=unigram
unigram_model_trainer.cc(494) LOG(INFO) Starts training with :
input: "data/wagahaiwa_nekodearu.txt"
model_prefix: "neko"
model_type: UNIGRAM
vocab_size: 8000
character_coverage: 0.9995
input_sentence_size: 10000000
mining_sentence_size: 2000000
training_sentence_size: 10000000
seed_sentencepiece_size: 1000000
shrinking_factor: 0.75
num_threads: 16
num_sub_iterations: 2
max_sentencepiece_length: 16
split_by_unicode_script: true
split_by_whitespace: true

trainer_interface.cc(109) LOG(INFO) Loading corpus: data/wagahaiwa_nekodearu.txt
trainer_size=0ace.cc(126) LOG(INFO) Loading: ▁吾輩は猫である
trainer_interface.cc(148) LOG(INFO) Loaded 2246 sentences
trainer_interface.cc(166) LOG(INFO) all chars count=182657
trainer_interface.cc(173) LOG(INFO) Done: 99.9502% characters are covered.
trainer_interface.cc(181) LOG(INFO) alphabet size=2507
trainer_interface.cc(211) LOG(INFO) Done! 2246 sentences are loaded
unigram_model_trainer.cc(121) LOG(INFO) Using 2246 sentences for making seed sentencepieces
unigram_model_trainer.cc(149) LOG(INFO) Making suffix array...
unigram_model_trainer.cc(153) LOG(INFO) Extracting frequent sub strings...
unigram_model_trainer.cc(204) LOG(INFO) Initialized 34678 seed sentencepieces
trainer_interface.cc(215) LOG(INFO) Tokenizing input sentences with whitespace: 2246
trainer_interface.cc(224) LOG(INFO) Done! 2193
unigram_model_trainer.cc(513) LOG(INFO) Using 2193 sentences for EM training
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=19246 obj=303.949 num_tokens=81388 num_tokens/piece=4.22883
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=17546 obj=276.547 num_tokens=81681 num_tokens/piece=4.65525
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=13133 obj=282.685 num_tokens=85486 num_tokens/piece=6.50925
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=13089 obj=279.841 num_tokens=85549 num_tokens/piece=6.53595
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=9814 obj=292.604 num_tokens=91023 num_tokens/piece=9.27481
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=9813 obj=289.388 num_tokens=91093 num_tokens/piece=9.28289
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=8799 obj=294.594 num_tokens=93339 num_tokens/piece=10.6079
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=8799 obj=293.481 num_tokens=93368 num_tokens/piece=10.6112
trainer_interface.cc(284) LOG(INFO) Saving model: neko.model
trainer_interface.cc(293) LOG(INFO) Saving vocabs: neko.vocab

学習が完了しました。適当な文章を入れて、分割の様子を見てみましょう。

# echo "吾輩は猫である。名はまだ無い。" | src/spm_encode --model=neko.
▁吾輩は 猫 である 。 名 はまだ 無い 。

# echo "吾輩は箱が好きで好きでたまらない。" | src/spm_encode --model
▁吾輩は 箱 が 好き で 好き で たま らない 。

# echo "ニャーと声を出せばで飯が提供され、ニャッと声を出せば頭を撫でられる。" | src/spm_encode --model=neko.model
▁ ニ ャ ー と 声を 出 せば で 飯 が 提 供 され 、 ニ ャ ッ と 声を 出 せば 頭 を撫 で ら れる 。

私自信の知識と能力が追いついていないのもあって、どんなところで使えそうなのかあまりイメージわかないですが、トークナイザの1つの選択肢として考えたいですねー!

関連リンク)
http://qiita.com/taku910/items/7e52f1e58d0ea6e7859c
どういった背景から作られたかが解説されています!

CSV ファイルで文字化けするんだけど!!!を調べた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

CSV ファイルってデフォルトでエクセルを使って開けるんですけど、どうにも文字コードを上手いことしてあげないとだめなんですよね。ということで幾つかの組み合わせについてぐぐったり、試したり、調べたのでメモ程度にー。
(ぐぐったらいろんな人が書いていて今更感もありますが・・・)

試したコードはこちら

<?php
// http://php.net/manual/ja/mbstring.supported-encodings.php
$encoding = [
	'UTF-8',
	'UTF-16BE',
	'UTF-16LE',
	'UTF-32BE',
	'UTF-32LE',
	'EUC-JP',
	'SJIS',
	'SJIS-win',
	'ISO-2022-JP',
	'CP932',
];

// https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E3%82%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%83%9E%E3%83%BC%E3%82%AF
$bom = [
	'UTF-8' => chr(0xEF) . chr(0xBB) . chr(0xBF),
	'UTF-16BE' => chr(0xFE) . chr(0xFF),
	'UTF-16LE' => chr(0xFF) . chr(0xFE),
	'UTF-32BE' => chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF),
	'UTF-32LE' => chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00),
];

$ary = ['abcdef', 'テスト', 'ほげほげ', '??'];
ob_start();
echo '"' . implode('","', $ary) . '"'; // とりあえず雑に...
$buf = ob_get_flush();

// bom なし
foreach($encoding as $e) {
	file_put_contents("{$e}.csv", mb_convert_encoding($buf, $e));
}

// bomあり版
foreach($encoding as $e) {
	if (empty($bom[$e])) {
		continue;
	}

	file_put_contents("{$e}_bom.csv", $bom[$e] . mb_convert_encoding($buf, $e));
}

寿司ビールが正しく出れば Unicode も適切に処理されているはずなので安心安全ですね!

では結果を順番に見ていきましょう。















まとめるとこんな感じですね!

・CP932 や SJIS, EUC-JP などでは案の定、寿司ビールは出ません。
・UTF では BOM ありでないと文字化けしてしまいました。
・UTF-16 では LE にする必要がありました。
・UTF-16 では他のものと異なり、自動でカンマ部分の解釈がされないようでした。
・UTF-32 はダメです。

というわけで、とりあえず UTF-8 BOM あり にしておけば、色々と安心安全に動くんじゃないかなーと思います。

または、どうしても表でほしいなら xlsx を直にいじくりまわす、ですかねー。
PHP なら phpexcel 。Java なら POI 。Python なら xlrd + pandas 。
などと様々な言語から xlsx を利用する手立てがあります(あたまの良い人ありがとう)ので、これらを使っていくことも検討すべきかと思います。まあ、つらいところもあるので、難しいものなら、そもそもいる?、なんて話にした方がいいよいかもしれませんね!

1 2 3 4 5 6 7 8 9 10 11 12 13