洋食の日記

「だ・である」調ではなく「です・ます」調で書きはじめれば良かったなと後悔してる人のブログです

インストール時にOpenBLASをビルドしてNumo::Linalgのバックグラウンドライブラリに使用するGemを作成した

はじめに

Numo::OpenBLASという、タイトルのとおりのものを作成した。

numo-openblas | RubyGems.org | your community gem host

使い方

Gemコマンドでインストールできる。このとき、OpenBLASをダウンロードしてビルドする。

$ gem install numo-openblas

使用方法はrequireするのみで、Numo::NArrayとNumo::Linalgもrequireされる。

require 'numo/openblas'

x = Numo::DFloat.new(5, 2).rand
c = x.transpose.dot(x)
eig_val, eig_vec = Numo::Linalg.eigh(c)

作った理由

Numo::Linalgでは、バックグラウンドライブラリのBLAS/LAPACKを選択することができる。しかし、この設定を間違うと、ライブラリを読み込めず失敗する。これに対して、以前、Autoloaderというものを作成した。これは、/usr/local/libなど、BLAS/LAPACKがいそうなディレクトリを探して、バックグラウンドライブラリとして設定する。OpenBLASをインストールしてある場合、だいたいが、これでうまくいくが、パッケージシステムによって、OpenBLASのビルドオプションが異なるという問題がある。OpenBLASはビルド時のオプションで、純粋にBLASAPIだけを提供するようにできる。これでビルドされた場合、Numo::Linalgのバックエンドライブラリとしては、OpenBLASの他にLAPACKも必要となる。このあたりどうなっているかを、利用者は把握しておく必要がある。また、Autoloaderは、実行速度のために、当たりをつけて検索しているので、ライブラリを見つけられないこともある。

対策法

上記のバックエンドライブラリの問題に対して、Gemのインストール時に、OpenBLASのダウンロードとビルドを行い、それをバックエンドライブラリとしてNumo::Linalgをロードすることにした。いまのところ、Rubyがインストールできたなら、OpenBLASをビルドできるであろう(gccとかなにかしらあるだろう)と想定している。

終わりに

Pythonのnumpyは、Anacondaのサーバーに各種プラットフォームでビルドされたOpenBLASがある。これをダウンロードすることも考えたがやめた。Rubyのデータ分析・機械学習が広まって、一定のコミュニティが形成された後に、Numo::Linalg用として提供されるのが理想かなと思った。また、OpenBLASは、CPUにあわせてビルドを行う。利用者の環境でビルドしたほうが、本来のパフォーマンスが出せるのではないかと思った。もろもろいい感じにしたDocker Imageを用意すれば良いんだろうけど、pip install numpy みたいな感じで、Numo::NArrayとNumo::Linalgをgemコマンドからインストールして、サクッと使いたかった(OpenBLASのビルドに時間がかかるけど...)。

github.com

Suikaの未知語処理のバグを修正した

はじめに

Pure Ruby形態素解析Suikaを作成したが、未知語処理がバグだらけで、リリースして1週間で4回もバージョンアップしてしまった。現在 ver. 0.1.4 だが、もう安定して動くはず。

suika | RubyGems.org | your community gem host

未知語処理の勘違い

バグの原因は、もともと試験実装していたスクリプトからの移設ミスだったりタイポだったりもあるが、根本的なところでの勘違いがあった。

MeCabの未知語処理の設定は、char.defに定義されている。このうち、文字種ごとに未知語処理をどう起動するかが定義されている部分がある。IPAdicでは以下のように定義されている(一部抜粋)。

...

KANJI        0 0 2
SYMBOL         1 1 0
NUMERIC        1 1 0
ALPHA        1 1 0
HIRAGANA       0 1 2
KATAKANA       1 1 2

...

行頭は文字種のカテゴリで「KANJI」は漢字を「KATAKANA」は片仮名を表す。その後ろの数字が、未知語処理の起動に重要となる。

文字種カテゴリ    動作タイミング グルーピング 長さ

動作タイミングは、1であれば、常に未知語処理を起動する。0であれば、現在チェックしている文字から始まる既知語がある場合は起動しない。グルーピングは、1であれば、同じ文字種でまとめ、0であれば、まとめない。長さは、まとめるときの未知語の長さを表す。0であれば、長さで未知語をまとめない。

勘違いしていたのは、グルーピングに「1」が指定されていた場合の、長さの指定で、例えば片仮名は以下の様になっているが、

KATAKANA       1 1 2

これの解釈は、片仮名の未知語が来た場合に2文字でまとめる、ではなく、グルーピングが「1」なので、とにかく同じ文字種でまとめる、が正解だった。つまり、グルーピングが「1」の場合には、後ろの長さは「0」を指定した場合と同様となる。片仮名の未知語が、2文字で分割されるので、どうしたものかと思っていたら、ここを勘違いしていた。

未知の文字コードによるバグ

くわえて「㋿」が与えられると、エラーになることがわかった。これは、本当に新しい未知の文字がきたときに、文字種認識メソッドが、nilを返していたのが原因だった。nilではなく、本来返すべき文字種カテゴリの「DEFAULT」を返すよう修正した。あわせて、㋿を他の年号の記号と同様に認識されるようにした。

irb(main):001:0> require 'suika'
=> true
irb(main):002:0> tagger = Suika::Tagger.new
irb(main):003:0> puts tagger.parse('時代は㍻から㋿へ')
時代    名詞,一般,*,*,*,*,時代,ジダイ,ジダイ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
㍻      名詞,サ変接続,*,*,*,*,*
から    助詞,格助詞,一般,*,*,*,から,カラ,カラ
㋿      名詞,サ変接続,*,*,*,*,*
へ      助詞,格助詞,一般,*,*,*,へ,ヘ,エ
=> nil
irb(main):004:0>

動作確認

Suikaに、様々な文章を与えて、思わぬエラーで落ちることなく処理を終えるかを確認したい。今回は、株式会社ロンウイットが提供する、livedoorニュースコーパスのデータをひたすら与えて、エラーで落ちないかを確認した。

require 'suika'

tagger = Suika::Tagger.new

Dir.glob('ldcc-20140209/text/*/*.txt').each do |filename|
  puts "--- #{filename}"
  File.foreach(filename) do |sentence|
    sentence.strip!
    puts tagger.parse(sentence) unless sentence.empty?
  end
end

これを実行したのが、以下のようになる。結果として、エラーで落ちることなく、無事に完走した。よほど変な文章を与えない限りは、Suikaは落ちないと考える。

f:id:yoshoku:20200711085350g:plain

おわりに

以上により、未知語が与えられるとエラーで落ちることがなくなった。ひととおり完成したので、次は、Suika::Tagger.newをした際の、辞書の読み込みが遅い問題を対処しようと思う。

github.com

SuikaというPure Rubyな形態素解析器を作成した

はじめに

Pure Ruby形態素解析Suikaを作成した。開発中でバッリバリにα版だが、思い切ってリリースすることにした。

suika | RubyGems.org | your community gem host

最も有名な形態素解析器であるMeCabもそうだが、形態素解析器は食べ物の名前がつくことが多い。「Rubyなので赤い食べ物が良いかな」と考えて、文字数とかわいらしさからSuika(スイカ)とした。

使い方

SuikaはPure Rubyで作られているため、MeCabをはじめ特別なライブラリを別途インストールする必要はない。

gem install suika

バッリバリにα版なので、機能はないに等しく、オプションなしのMeCabコマンドと同様となる。

$ irb
irb(main):001:0> require 'suika'
=> true
irb(main):002:0> tagger = Suika::Tagger.new
irb(main):003:0> puts tagger.parse('すもももももももものうち')
すもも  名詞, 一般, *, *, *, *, すもも, スモモ, スモモ
も      助詞, 係助詞, *, *, *, *, も, モ, モ
もも    名詞, 一般, *, *, *, *, もも, モモ, モモ
も      助詞, 係助詞, *, *, *, *, も, モ, モ
もも    名詞, 一般, *, *, *, *, もも, モモ, モモ
の      助詞, 連体化, *, *, *, *, の, ノ, ノ
うち    名詞, 非自立, 副詞可能, *, *, *, うち, ウチ, ウチ
=> nil

Taggerのparseメソッドは、形態素解析した結果を、StringのArrayで返す。

irb(main):004:0> tagger.parse('すもももももももものうち')
=> ["すもも\t名詞, 一般, *, *, *, *, すもも, スモモ, スモモ", "\t助詞, 係助詞, *, *, *, *, も, モ, モ", 
"もも\t名詞, 一般, *, *, *, *, もも, モモ, モモ", "\t助詞, 係助詞, *, *, *, *, も, モ, モ", 
"もも\t名詞, 一般, *, *, *, *, もも, モモ, モモ", "\t助詞, 連体化, *, *, *, *, の, ノ, ノ", 
"うち\t名詞, 非自立, 副詞可能, *, *, *, うち, ウチ, ウチ"]

実装のはなし

MeCabの作者であるTaku Kudoさんが書かれた、形態素解析器に関する本を読んで、勉強のために実装した。 ホントにわかりやすい本で、ホントすごい(語彙)。

形態素解析器の実装では、候補となる単語の検索のための、トライ木の実装が重要になるのだが、Rubyでトライ木を実装したGemがすでにあり(自作してた拙いモノはスッパリ捨てて)これを使うことにした。動作も早く、ホントすごい。

github.com

さらに、単語の品詞推定などには辞書が必要になる。これについては、Pure Pythonで書かれたJanomeという形態素解析器を参考にした (Pure Pythonで動作も軽いJanomeの存在はSuikaを開発する気合を与えてくれた)。

github.com

JanomeはIPAdicによるバイナリ辞書を同梱している。Suikaでも、IPAdicをパースしてRuby Hashにしたりしたモノを持たせることにした。Suika::Tagger.newすると、この同梱したバイナリ辞書を読み込むのだが、ここがとっても遅いので、なんとか改善したい。

おわりに

最初はRumaleの応用も兼ねて、点推定による形態素解析器(単語分割などを機械学習で行う)を遊びで作っていた。作っていると、段々とそれなりに動くものが欲しくなって、ここまで来てしまった。Ruby形態素解析する場合は、FFIMeCabを叩くNattoというGemが有名で、これをオススメする。Suikaは、まだ、遊んでみるには良いけど、使ってはいけない。

Rubyには、PythonのNLTKやSpaCyのような包括的な自然言語処理ライブラリで、コレだ!!というのはまだない。RSpecなどがそうだが、自然言語的にかけることを意識するRubyの文化と、自然言語処理は相性が良いと思っている。Ruby自然言語処理ライブラリを夢見て、Suikaの開発は、少しずつでも続けていきたいと思っている。

github.com

画像をNumo::NArrayで扱えるMagroにフィルタを追加した

はじめに

Rubyの画像処理ライブラリMagroに、フィルタをかけるメソッドを追加し、version 0.3.0としてリリースした。

magro | RubyGems.org | your community gem host

使い方

Magroは画像ファイルの読み書きにlibpngとlibjpegを必要とする。Magroをインストールすると、依存関係でNumo::NArrayがインストールされる。

$ brew install libpng libjpeg
$ gem install magro

フィルタを使うことでエンボス加工っぽいことをしてみる。

require 'magro'

img = Magro::IO.imread('sample.jpg')

kernel = Numo::DFloat[
  [ 1, 2, 1],
  [ 0, 0, 0],
  [-1,-2,-1]
]
img = Magro::Filter.filter2d(img, kernel, scale: 1, offset: 128)

Magro::IO.imsave('emboss.png', img)

Magro::Filter.filter2dメソッドに、画像とフィルタカーネルを渡す。引数としてscaleとoffsetがある。scaleはフィルタカーネルを正規化する値、offsetはフィルタ後に足す値となる。

はてなでアイコンに使用しているハンバーグの画像を、上記スクリプトに入れると次のようになる。

f:id:yoshoku:20200510013338p:plain

おわりに

追加したのは、フィルタをかけるためのメソッドのみで、ぼかしや先鋭化といったメソッドは追加しなかった。最初は、Pillowライクなものも検討したが、あれこれとフィルタを駆使するようなこともないかと思い、すっかり外してしまった。フィルタをかけるための畳み込みの実装には、いわゆるim2colを使用した(画像のブロックを並べて行列表現にすることで、画像とフィルタの畳み込みを行列積で実現する技法)。画像をNumo::NArrayで表現しているので、容易に実装できた。今後もできるだけミニマルに、できるだけRubyで実装してみようと思う。

github.com

画像をNumo::NArrayで扱えるMagroに画像のサイズ変更を追加した

はじめに

Magroという画像をNumo::NArrayで扱える(読み書きできる)Gemを作っていたが、いろいろと時間がとれず更新が滞っていた。

yoshoku.hatenablog.com

せっかくの連休なので、Bilinear補完によるサイズ変更を追加した。基本的なとこから実装するならばNearest Neighbor法であるし、クオリティを重視するならばLanczos法であるが、実装しやすさからBilinear補完にした。OpenCVのresizeのデフォルトがBliinearなのもある。

使い方

Magroは画像ファイルの読み書きにlibpngとlibjpegを必要とする。Magroをインストールすると、依存関係でNumo::NArrayがインストールされる。

$ brew install libpng libjpeg
$ gem install magro

画像の読み込み・サイズ変更・保存までは以下のようになる。

require 'magro'

img = Magro::IO.imread('hoge.png')

resized = Magro::Transform.resize(img, height: 128, width: 128)

Magro::IO.imsave('hoge_resized.png', resized)

おわりに

Bilinear補完によるサイズ変更の実装では、Extensionは使わずにすべてRubyで書いてある。画像をNumo::NArrayで扱える便利さを、自身で体験することになった。次に追加するとすればフィルタかな。

github.com