サーバーサイドエンジニアの芹沢です。
以前書いた私のエントリーにて、弊社のRubyKaigi参加者がそれぞれレポートを書くよという話をしましたが、その2人目のレポートです。
私のレポートでは、2日目に行われたセッションから、Progress of Ruby/Numo: Numerical Computing for Rubyを紹介させていただきたいと思います。
スライドはこちら
Ruby/Numo Projectとは
Rubyで機械学習や科学技術計算を行うためのライブラリを開発しているプロジェクトです。
一般に、機械学習界隈ではPythonで実装されたライブラリがよく使われていると言われており、実際私もnotebookやpandasをよく使うのですが、そういったライブラリが解決することをrubyでも解決することを目指しているプロジェクトのようです。*1
Rubyで機械学習を行うための別のアプローチとして、PythonのコードをRubyでも実行可能にするためのbinding機構であるPyCallがあります。今回のRubyKaigiでもセッションとワークショップが行われましたが、こちらはNumpy, pandasといったPythonのライブラリ資産をそのままrubyでも実行できるようにbindingすることを目指しています。
現在複数のgithub repositoryがこのプロジェクト配下に存在しますが、今回のセッションでも説明が多かったNumo::NArrayについて紹介します。
Numo::NArrayとは
多次元配列の計算を高速に行うためのライブラリ(という理解です。。)です。RubyにもArrayやMatricsの標準ライブラリがありますが、用意されているメソッドはPythonの行列計算ライブラリであるNumpyに近いインターフェースとなっています。
numpyとの対応表がwikiにありますが、かなりの割合のnumpyのメソッドを網羅していることが分かります。実際、Numpyの363個ある関数のうち217個をカバーしており、今後さらに91個が追加予定になっているとのことです。
Numo::NArrayを手元で試す
実際にNumo::NArrayを手元で動かしてみました。
Numo::NArrayのgithub repositoryのREADMEにはspecific_installを使う方法が記載されていますが、普通にGemfileを作ってbundle installしてもインストールできるのでこちらを採用しました。
こんな感じのGemfileを書いて bundle install
します。
# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem 'numo-narray', git: 'https://github.com/ruby-numo/narray.git'
irbで動かしてみます。
irb(main):001:0> require 'numo/narray' => true irb(main):002:0> a = Numo::NArray[[1,2,3,4,5]] => Numo::Int32#shape=[1,5] [[1, 2, 3, 4, 5]] irb(main):003:0> a * 2 => Numo::Int32#shape=[1,3] [[2, 4, 6, 8, 10]] irb(main):004:0> a.shape => [1, 5] irb(main):005:0> b = Numo::NArray[[2],[3]] => Numo::Int32#shape=[2,1] [[2], [3]] irb(main):006:0> c = a * b => Numo::Int32#shape=[2,5] [[2, 4, 6, 8, 10], [3, 6, 9, 12, 15]]
最後の行でやっている計算はbroadcastingという機能を使って実現されています。
これは、行列同士の四則演算を行う際に、両者の行列の形状が異なる場合であっても、次元が少ない方の配列を拡張して計算できるようにしてくれるものです。
これをrubyのArrayだけでやろうとすると結構複雑なmap処理をしないと実現できませんがNumo::NArrayを使うとシンプルに記述することができます。
また、今回のセッション中でも紹介されていたマスク処理という機能があります。これは配列内の各値に対して、条件にマッチしれいれば1,そうでなければ0の配列を返す機能です。
irb(main):001:0> a = Numo::NArray[1..10] => Numo::Int32#shape=[10] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] irb(main):002:0> a < 6 => Numo::Bit#shape=[10] [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
これと同じことをRubyで書こうとすると以下のようになると思います。
a = (1..10).map { |i| i < 6 ? 1 : 0 }
ステップ関数を配列の全要素に対して適用して二値ベクトルを得る時などに直感的に書けそうな感じがしてこのsyntaxは個人的にはすごく気に入りました。
ですが、パフォーマンスが悪ければ実用は厳しいので、実際にこのマスク処理のベンチマークを取ってArrayのみで実装した場合と比較してみました。
require 'numo/narray' require 'benchmark' base = (1...1000_000).to_a compare = 1000 nbase = Numo::NArray[base] Benchmark.bm do |r| r.report 'narray' do (nbase < compare).to_a end r.report 'map' do base.map do |v| v < compare ? 1 : 0 end end end
user system total real narray 0.030000 0.010000 0.040000 ( 0.029366) map 0.060000 0.010000 0.070000 ( 0.087115)
Numo::Int32のMask処理の方がArrayのmap処理のみで実装したサンプルよりも2倍ほど早い結果が出ました。
ただ、勘のいい人はお気づきのように、これはrubyのArrayオブジェクトをNumo::Int32のオブジェクトに変換する処理をBenchmark対象に入れていない場合の結果です。変換処理もベンチマーク対象に含めてみると
user system total real narray 0.140000 0.000000 0.140000 ( 0.156649) map 0.070000 0.010000 0.080000 ( 0.077548)
ベンチマーク結果が逆転します。Numo::Int32オブジェクトに変換する処理はかなりオーバーヘッドが大きい処理と考えられます。そのため、オーバーヘッドを無視できるほど同じ配列を何度も使用するようなケースであればともかく、基本的にはパフォーマンス面ではArrayを使った方が有利のようです。
まとめ
簡単ではありますがNumo::NArrayの紹介をさせていただきました。普段Pythonでscriptを書くこともある身としては、RubyでもNumpyのsyntaxで配列操作をできるようになるのはとてもありがたいことなので、projectの今後の発展に期待したいところです。
また、Arrayからのオブジェクト生成時のオーバーヘッドが大きいと本文中に書いたものの、それを無視できるユースケースであればNumo::NArrayの配列計算は相当パフォーマンスが良いと思われるため、うまくハマるユースケースがあれば使っていきたいと思います。
なお、今回のカンファレンスは会社の経費で参加させて頂きました。今年のRubyKaigiのclosingセッションでも「仕事としてRubyKaigiに参加している人はいますか?」というa_matsudaさんの質問に沢山の手が挙がる様子が見受けられましたが、このようなカンファレンスに業務として参加させていただけることはとてもありがたいことだと思います。
最後に、お約束になりますがトレタはサーバーサイドエンジニアを募集しております。来年のRubyKaigiは仙台なので、牛タンと萩の月と笹かまぼこが食べたい方は是非ご応募ください。*2(そうでない方もご応募ください) www.wantedly.com