decadence

個人のメモ帳

MeCab, CaboCha で楽々自然言語処理 (Mac + Perl)

これ何

PerlMeCab や CaboCha を使う際の適当にメモ
MeCab に関しては情報が多いが CaboCha はあまり無いので Mac + Perl でしたい方は気持ち参考にして頂けたらと思う

特に自然言語処理の研究がメインではないが、文書構造など適度に取得して使いたい分野
全く触れた事無い人でも便利ライブラリを使う事で以下のように色々遊べる
書く人が書けば以下のような適当なのでもブクマ稼げる

文字コードは基本的に utf-8 を使いたい。

MeCab

文を入力に、単語を分割して名詞が、形容詞が、といった結果を出す形態素解析

  • インストール
    • デフォルトでutf-8の辞書が入る
brew install mecab mecab-ipadic
  • Perl で使う際の注意
cpanm --interactive Text::MeCab
#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use utf8;

use Text::MeCab;
use Encode qw/decode_utf8 encode_utf8/;

my $text = "すもももももももものうち";
my $mecab = Text::MeCab->new;

my $node = $mecab->parse($text);
while($node) {
    # コード内ではUTF-8フラグの立っている文字列を扱う
    my $surface = decode_utf8 $node->surface; # すもも
    my $feature = decode_utf8 $node->feature; # 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
    # 出力する際はUTF-8フラグを取り除く
    say encode_utf8 $surface;
    say encode_utf8 $feature;
    $node = $node->next;
}

__END__

UTF-8フラグについては、Perl で utf8 化けしたときにどうしたらいいか - blog.64p.org

入り口で decode して、内部ではすべて flagged utf8 で扱い、出口で encode する。これがすべてです!とにかくこの基本方針をまもっていれば幸せになれます。

プログラム中では `use utf8` で内部文字列はUTF-8フラグを立てたものを使い、入り口で decode 、出口で encode
で、Text::MeCab は Devel::Peek すると分かるが、 UTF-8 フラグの立ってない結果を返すので、外から来たものとして扱い、中で用いる際には decode してから使う。出力するならまた encode する
注意点として、例えば TFIDF とか計算する際に日本語を hash の key にしたいが、UTF-8 フラグの立った文字は key に出来ないので注意

また、通常の辞書だけでなく Wikipediaはてなキーワード といったものを扱う事もできる
昔ので Macports とか使ってるけど一応参考記事はこっち Mecab dictionary, with cmecab-java - krrrr38.com
Perl から固有辞書を使う際は `$mecab = Text::MeCab->new({ userdic => 'mydic' })` とか

CaboCha

文を入力に、「A が B した。」に対して 「A → B」といった結果を出す係り受け解析器

  • インストール
    • mecab に依存した CaboCha が入る
brew cabocha
cabocha --version 

CPAN に CaboCha のラッパーライブラリは存在しないが、元々用意されているのでcabocha - Yet Another Japanese Dependency Structure Analyzer - Google Project Hostingから version の等しい tar.bz2 とか落として解凍

cd cabocha-x.xx/perl
cpanm .

動作確認

perl test.pl

形態素解析結果はも取れたりするのだが、上記のようなものが取れたら満足なのではないかと思う
特に Document など無いので必要そうなのだけ考える
中に含まれる CaboCha.pm を見れば良いのだが、依存クラスとして以下のようなものがある

  • CaboCha::Parser
    • 文を入力に Parser で parse する事で結果 (Tree) が得られる
  • CaboCha::Tree
    • Parser で得られた結果。係り受け構造は test.pl を見て分かるが木のようになる。
  • CaboCha::Chunk
    • 係り受け構造の文節単位
    • 例:"すももも" - "ももも" - "ももの" - "うち" (chunk size = 4)
  • CaboCha::Token
    • 形態素解析結果の最小単位
    • 例 : "「すもも」「も」" - "「もも」「も」" - "「もも」「の」" - "「うち」" (token size = 7)
    • chunk といった文節の中に語がいくつ含まれるといった情報も保持される
#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use utf8;

use CaboCha;

my $text = "すもももももももものうち";
my $parser = CaboCha::Parser->new;

my $tree = $parser->parse($text);
my $chunk_number = 0;
for (my $i = 0; $i < $tree->chunk_size; ++$i) {
    my $chunk = $tree->chunk($i);

    # chunk 番号は数え上げる。係り受け先は link で取得
    say sprintf("chunk %d is following chunk %d", $chunk_number++, $chunk->{link});

    # token_size で chunk に含まれる token の数が取れる
    # token_pos で chunk に含まれる token の先頭の index が取れる
    for(my $j = 0; $j < $chunk->{token_size}; ++$j){
        my $token = $tree->token($chunk->{token_pos} + $j); # token

        # ここで得られる値も UTF-8 は立っていないので注意
        say "\t" . $token->{surface} . " : " . $token->{feature};
    }
}

__END__

Token から Chunk を得る事も可能なので、先に形態素解析結果を見た後に名詞がどの文節にかかるか確認したいなどといった場合には、以下のようにしても良い
UTF-8 の立った文字列は decode した上で処理を行うのを忘れないよう

for (my $i = 0; $i < $tree->token_size; ++$i) {
    my $token = $tree->token($i);
    my $feature = decode_utf8($token->{feature});
    if ($feature =~ /^名詞/) {
        my $chunk = $token->{chunk};
        say $token->{surface} . " is following chunk " . $chunk->{link};
    }
}

`$chunk->{link}` が -1 になると、文が終わりとなる
"。" などで区切られた文に対しても CaboCha は係り受け構造があると認めるので、一応気にしておくべきな気がする

一応ではあるが Cabocha.pm に対して、 Token の `*swig_surface_get` といった記述に対して `$token->{surface}` や `$token->swig_surface_get()` がそのまま扱える

メモ(SQL::Makerでのjoin,JSON::PP::Boolean)

SQL::Makerでのjoin

いつも分からない,ってか無理☆だけど一応

欲しいSQL

SELECT comment.*, evaluation
    FROM comment LEFT JOIN comment_evaluation
        ON comment.uid = comment_evaluation.comment_id
    WHERE question_id = 1
    ORDER BY comment.created DESC;

SQL::Maker使う

my $builder = SQL::Maker->new(driver => 'mysql');
$builder->select(
    undef,
    ['comment.*', 'evaluation'],
    { question_id => '1'},
    {
        order_by => 'created DESC',
        joins => [
            [comment => {
                type => 'LEFT',
                table => 'comment_evaluation',
                condition => 'comment.uid = comment_evaluation.comment_id'
            }],
        ],
    }
);

JSON::PP::Boolean

JSON::XS使ってjsonパースすると,bool値について,前までJSON::XS::Booleanが返ってきた.
これは直接以下のように書けた

if ($hoge){}

昨日JSON::XSが3.0になった際に,bool値がJSON::PP::Booleanで返ってくるようになった.
これの条件判断は上記のように書いたままでは通らない.
実際に判定する際は以下のように書く

if ($$hoge){}

詳しい事は知らないが誰得

Amon2 project template

Amon2を使う

こんな感じで使えば自分にとって使いやすいんじゃなかろうかってメモ
Response周り整理したい気がする
何かあれば適宜書き加えてく

基本

PLACK_ENV

  • deployment
  • development (default)
  • test

$ENV{PLACK_ENV}で管理されてる環境変数
起動時オプションは以下の通り.

plackup -E deployment app.psgi

静的ファイル

静的ファイルはpsgiファイル見ればわかるが,Plack::Middleware::Staticによって/static/に割り当てられてる.

mysql

config/$ENV{PLACK_ENV}.pl内に,DBI->connectに渡す各引数を記載
DBIにはDBIx::Sunnyを利用.関数名が直感的で良い.SQL::NamedPlaceholderとか使って拡張した独自のDBI使っても良い.
DBIx::Sunnyとか継承させたものをHoge::DBIとすると,Hoge::DBI::dbをuse parent -norequire => 'DBIx:Sunny::db'で作りselect_oneとか独自実装,Hoge::DBI::stをuse parent -norequire => 'DBIx::Sunny::st'で作る.後は下のRootClassで読み込ませる.

+{
    'DBI' => [
        "dbi:mysql:database=fuga", 'username', 'password',
        +{
            RootClass         => 'DBIx::Sunny',
            mysql_enable_utf8 => 1,
        }
    ],
};

setup

cpanfileに依存書く.

carton install
mysql -uusername -p fuga < db/mysql.sql

最初にする事

作る

amon2-setup.pl --flavor=Basic Hoge

lib/Hoge/Web以下に3つのフォルダ作成

cd Hoge/lib/Hoge/Web
mkdir C Model Service

Routing

H::W::Dispatcherを以下のように変更

- use Amon2::Web::Dispatcher::Lite
+ use Amon2::Web::Dispatcher::RouterSimple;

+ use Hoge::Web::C::Root;

+ connect '/'   => 'Root#index';

Root#indexはこんな感じ

sub index {
    my ($class, $c) = @_;
    return $c->render('index.tt');
}

A::W::D::RouterSimpleはRouter::Simple - search.cpan.orgこっち見れば大体いける.

MVC

Controller

Hoge::Web::C以下に記述

Model

Hoge::Web::Model以下に記述
テーブルモデル(+データモデル)

Service

Hoge::Web::Service以下に記述
DBアクセス

View

tmplの中に.出力するものは.html,wrapper,includeするものは.ttにする

テスト

全てのテストで読み込む汎用パッケージを用意する.Amon2ではt::Utilがあるから,これを読み込む
以下のような記述があるため,これでtest時はtest用のdatabaseを参照するようになる.

BEGIN {
    $ENV{PLACK_ENV} = 'test';
}
  • 参考

共通してuseするものとか書いておくと楽
Hatena-Textbook/db-control-by-dbi.md at master · hatena/Hatena-Textbook · GitHub

Middleware

適当にlib以下にPlack::Middleware::MyMiddlewareとか作る
Plack::Middleware継承させて,前処理はprepare_appに,毎回呼ばれる処理はcallで定義.
後はapp.psgiでenableするだけ.引数のハッシュはそのままMyMiddlewareのselfから呼び出せる

その他

Context関連

redirectとか色々
$c->redirect('/foo', +{bar => 3})

取り敢えずドキュメント見る.Amon2::Web - search.cpan.org

rendering
  • render_json

Web.pmのload_pluginsに'Web::JSON'を付け加えるとContextからrender_jsonが呼べる.
Amon2::Plugin::Web::JSON - search.cpan.org
Web.pmに直接実装加えても良さそう.その場合はContextからcreate_response使って組み立てる感じ.

jsonの型付けにはJSON::Types - search.cpan.orgを使おう

Context拡張

Hoge::Web::Base.pmにWeb.pmの中身移して,Web.pmを拡張として用いるのが良さそう.
(Hoge::Web::Context.pmにWeb.pm継承させてapp.psgiで読み込ませたら出来そうだったけどできなんだ...)

  • パラメータ周り

wantarray使って複数値と単一値のどちらを取るか決めるみたいなのしてみたけど,Perl Monger的にはどうなんだろう.使いやすいっちゃ使いやすいと思うのだが.

sub string_param {
    my ($self, $key) = @_;

    if (wantarray) {
      map {decode_utf8 $_} $self->_parameters->get_all($key);
    }
    else {
      decode_utf8 $self->_parameters->get($key) // "";
    }
}

sub _parameters {
    my $self = shift;

    $self->{_parameters} //= do {
        my $query = $self->request->query_parameters;
        my $body  = $self->request->body_parameters;
        my $path  = Hash::MultiValue->new(%{$self->{args}});
        Hash::MultiValue->new($query->flatten, $body->flatten, $path->flatten);
    };
}

追記

毎回考えるの面倒だし,自分用のAmon2のFlavor作った
一応使えるけど,目下改良中
krrrr38/Amon2-Setup-Flavor-Krrrr · GitHub