Red Data Tools¶ ↑
: subtitle
楽しく実装すればいいじゃんねー
: author
須藤功平
: institution
株式会社クリアコード
: content-source
沖縄Ruby会議02
: date
2018-03-10
: start-time
2018-03-10T13:10:00+09:00
: end-time
2018-03-10T14:00:00+09:00
: theme
clear-code
Red Data Tools¶ ↑
プロジェクト
実現したいこと¶ ↑
Rubyでn データ処理
やっていること¶ ↑
データ処理用のn ツールの開発
開発例¶ ↑
* 各種フォーマットを扱うgem * Apache Arrow, Apache Parquet, CSV, ... * 各種パッケージの用意 * deb, RPM, Homebrew, ... * ...
この話の目的¶ ↑
勧誘n (('note:一緒に開発しようぜ!'))
勧誘方法¶ ↑
* プロジェクトのポリシーを紹介 * 一緒に活動したくなる! * 開発の具体例を紹介 * 一緒に開発したくなる!
ポリシー1¶ ↑
(('tag:center')) (('tag:large')) Rubyコミュニティーをn 超えて協力するn (('note:もちろんRubyコミュニティーとも協力する'))
スライドプロパティー¶ ↑
: as-large-as-possible
false
Rubyに閉じずに協力¶ ↑
* 他の言語は敵ではない * 他の言語がよくなることは\n Rubyがなにかを失うことではない * みんなよくなったらいいじゃん * Rubyも他の言語も
協力例n(('note:Apache Arrow'))¶ ↑
* Pythonの人達他と一緒に\n C/C++のライブラリーを開発 * それぞれでバインディングを開発 * それぞれで同じライブラリーを活用
ポリシー2¶ ↑
(('tag:center')) (('tag:large')) 非難することよりもn 手を動かすことが大事
スライドプロパティー¶ ↑
: as-large-as-possible
false
非難しない¶ ↑
そんなことをn しているn 時間はない
手を動かす¶ ↑
* これ、よくないなー * よくすればいいじゃんねー * これがないからなー * 作ればいいじゃんねー
ポリシー3¶ ↑
(('tag:center')) (('tag:large')) 一回だけの活発な活動よりn 小さくてもn 継続的に活動することがn 大事
スライドプロパティー¶ ↑
: as-large-as-possible
false
一回だけの活発な活動¶ ↑
* ○○作ったよ!どーん! * すごい! * (('note:…数年後…'))今動かないんだよね…
継続的な活動¶ ↑
* ちまちま○○作ってるんだー! * がんばってね!今後に期待! * (('note:…数年後…'))これは…使える!
継続的な活動のために¶ ↑
* がんばり過ぎない * 短距離走ではなくマラソン * 途中で休んだっていい * 1人で抱え込まない * みんなでやれば途中で休みやすい
ポリシー4¶ ↑
(('tag:center')) (('tag:large')) 現時点での知識不足はn 問題ではない
スライドプロパティー¶ ↑
: as-large-as-possible
false
知識不足?¶ ↑
* 高速な実装の実現 * プログラミング・数学などの\n 高度な知識は便利 * 今すごい人でも\n 最初はなにも知らなかった * 「今知らないこと」は\n 「始めない理由」にはならない
私たちは学べる¶ ↑
* 知識は身につく * 活動していく中で自然と * 学び方 * OSSの既存実装から学習 * ドキュメントを読む * 他の人から教えてもらう
ポリシー5¶ ↑
(('tag:center')) (('tag:large')) 部外者からの非難はn 気にしない
スライドプロパティー¶ ↑
: as-large-as-possible
false
部外者からの非難¶ ↑
* Rubyで頑張ってもアレだよねー\n とか * 無視する * 対応している時間はない
ポリシー6¶ ↑
楽しくやろう!
楽しむ¶ ↑
使っているのはn Rubyだから!
ポリシー¶ ↑
* Ruby外とも協力 * 非難するより手を動かす * 継続的な活動 * 知識不足は問題ない * 外からの非難は気にしない * 楽しくやろう!
開発例¶ ↑
* ツールの紹介((*ではない*)) * 実装の紹介 * 開発中に((*おっ*))と思ったやつとか * 紹介したいやつとか
開発例1¶ ↑
csv
csv¶ ↑
* CSVの読み書きライブラリー * 2003年から標準添付 * 2018年からメンテナンスを引取
(({dig}))を追加したとき¶ ↑
(('tag:x-small'))((<URL:github.com/ruby/csv/pull/15>))
* (({CSV::Table#dig})) * (({CSV::Row#dig}))
(({dig}))の作り方¶ ↑
# rouge ruby def dig(index, *indexes) # ここだけ違う value = find_value(index) # ↓は共通 return nil if value.nil? return value if indexes.empty? value.dig(*indexes) end
(({CSV::Row}))での(({find_value}))¶ ↑
# rouge ruby row[index] # => field value
(('tag:center')) (({dig}))の中では(({self}))の(({[]}))を呼べばよい
(({[]}))の呼び方¶ ↑
# rouge ruby def dig(index, *indexes) # インスタンスメソッドでは # selfを省略できるからこう? value = [index] # ↑は配列リテラル # ... end
(({[]}))の呼び方¶ ↑
# rouge ruby def dig(index, *indexes) # こういうときこそsendで # メソッド呼び出し value = send(:[], index) # 動く # ... end
(({[]}))の呼び方¶ ↑
# rouge ruby def dig(index, *indexes) # row[index]みたいに書けば # よかった value = self[index] # もちろん動く # ... end
(({self}))の(({[]}))の呼び方¶ ↑
* 自分も昔悩んだ気がする * 懐かしかったので紹介 * みんなで開発🠊気づきやすい
開発例2¶ ↑
Red Datasets
Red Datasets¶ ↑
* データを簡単に使えるように! * 実験・開発に便利
例:日本語データ欲しい!¶ ↑
# rouge ruby require "datasets" # 日本語版Wikipediaの記事データ options = {language: :ja, type: :articles} dataset = Datasets::Wikipedia.new(options) dataset.each do |page| # 全ページを順に処理 p page.title # タイトル p page.revision.text # 本文 end # インターフェイスがeachなのがカッコいいんだよ!
実装方法¶ ↑
(1) データのダウンロード (2) データのパース (3) 順に(({yield}))
ダウンロード¶ ↑
# rouge ruby require "open-uri" open("https://...") do |input| File.open("...", "wb") do |output| IO.copy_stream(input, output) end end
途中でエラーになったら?¶ ↑
(('tag:center')) (('tag:x-large')) やり直し
(('tag:center')) または
(('tag:center')) (('tag:x-large')) 再開
再開¶ ↑
HTTPn range request
HTTP range request¶ ↑
* リクエスト * (({Range: bytes=#{start}-})) * レスポンス * (({206 Partial Content})) * (({Content-Range: bytes ...}))
open-uriとrange request¶ ↑
* open-uriはrange request未対応 * 出力無関係のAPIだからしょうがない * どうなるのがいいだろう? * 今度田中さんに相談しよう * Red Data ToolsはRubyもよくしたい\n (('note:ポリシー1:コミュニティーを超えて協力'))
open-uriでrange request¶ ↑
# rouge ruby # 文字列キーはHTTPヘッダーになる options = {"Range" => "bytes=#{start}-"} open("...", options) do |input| # レスポンスが200か206かはわからない File.open("...", "ab") do |output| IO.copy_stream(input, output) end end
大きなデータの扱い¶ ↑
* 時間がかかる * あとどのくらいか気になる * ちゃんと動いているよね…?
あとどのくらい?¶ ↑
プログレスバー
プログレスバーの実装¶ ↑
# rouge ruby n = 40 1.upto(n) do |i| print("\r|%-*s|" % [n, "*" * i]) sleep(0.1) end puts
sprintfフォーマット¶ ↑
(('tag:center')) (({%-*s}))
* (({%})):フォーマット開始 * (({-})):左詰め * (({*})):引数で幅を指定 * (({s})):対象は文字列
sprintfフォーマット¶ ↑
(('tag:center')) (({“%-*s” % [n, “*” * i]}))
* 幅は(({n}))桁 * (({"*" * i}))を左詰め
open-uriでプログレスバー¶ ↑
# rouge ruby length = nil progress = lambda do |current| ratio = current / length.to_f print("\r|%-10s|" % ["*" * (ratio * 10).ceil]) end open(uri, content_length_proc: ->(l) {length = l}, progress_proc: progress) do |input| # ... end puts
もっとプログレスバー¶ ↑
* バックグランド化したら? * 表示して欲しくない\n (('note:ヒント:プロセスグループ')) * リダイレクトしているときは? * 表示して欲しくない\n (('note:ヒント:IO#tty?')) * プログレスバーの表示幅は?\n (('note:ヒント:io/console/size'))
開発例3¶ ↑
Apache Arrown Red Arrow
Apache Arrow¶ ↑
* インメモリーデータ分析用\n データフォーマット\n (('note:ほぼ固まってきた')) * インメモリーデータ分析用\n 高速なデータ操作実装\n (('note:徐々に実装が始まっている')) * 今、すごくアツい!
Apache Arrowの特徴¶ ↑
(('tag:center')) (('tag:large')) データ交換コストが低い
スライドプロパティー¶ ↑
: as-large-as-possible
false
低データ交換コスト¶ ↑
* 複数システムで協力しやすい * Rubyでデータ取得🠊Pythonで分析 * 徐々にRubyを使えるところを\n 増やせる
Apache Arrowの利用例¶ ↑
* Scala🤝Python * (('tag:x-small'))Apache Spark * CPU🤝GPU * (('tag:x-small'))((<URL:https://github.com/gpuopenanalytics/libgdf>)) * CPU🤝FPGA * (('tag:x-small'))((<URL:https://github.com/johanpel/fletcher>))
Apache ArrowをRubyでも!¶ ↑
* バインディングを本体で開発 * 本体の開発チームに入った\n * GObject Introspection(GI)を利用 * GIを使うとRuby以外のバインディングも\n 自動生成できる\n (('note:ポリシー1:コミュニティーを超えて協力'))
Red Arrow¶ ↑
* Apache Arrowの\n Rubyバインディング * GIベースのバインディングに\n Ruby特有の機能をプラス
Ruby特有の機能例¶ ↑
スライスAPI
スライス¶ ↑
(('tag:center')) データの一部を切り出す
# rouge ruby (0..10).to_a.slice(2) # => 2 (0..10).to_a.slice(2..4) # => [2, 3, 4] (0..10).to_a.slice(2, 4) # => [2, 3, 4, 5]
Red Arrowのスライス¶ ↑
# rouge ruby table.slice(2) # 2行目だけのテーブル # Array#sliceと違う
Red Arrowのスライス¶ ↑
# rouge ruby table.slice(2..4) # 2,3,4行目だけのテーブル # Array#sliceと同じ
Red Arrowのスライス¶ ↑
# rouge ruby table.slice(2, 4) # 2,4行目だけのテーブル # Array#sliceと違う table.slice(2, 4, 6, 8) # 2,4,6,8行目だけのテーブル # Array#sliceと違う
Red Arrowのスライス¶ ↑
# rouge ruby table.slice([2, 4]) # 2,3,4,5行目だけのテーブル # Array#slice(2, 4)と同じ
Red Arrowのスライス¶ ↑
# rouge ruby table.slice([true, false] * 5) # 0,2,4,6,8行目だけのテーブル
Red Arrowのスライス¶ ↑
# rouge ruby table.slice do |slicer| slicer.price >= 500 end # priceカラムの値が500以上の # 行だけのテーブル
Red Arrowのスライス¶ ↑
# rouge ruby table.slice do |slicer| (slicer.price >= 500) & (slicer.is_published) end # priceカラムの値が500以上かつ # is_publishedカラムの値がtrueの # 行だけのテーブル
Red Arrowのスライス¶ ↑
Active Recordみたいなfluent intefaceよりもブロック内で式を書く方がRubyっぽいんじゃないかと思うんだよねーn fluent interfaceはORを書きにくいからさー
(('tag:xx-small')) Fluent interface:n ((<URL:bliki-ja.github.io/FluentInterface/>))
ブロック内で条件式¶ ↑
# rouge ruby class Arrow::Table # table.slice {|slicer| ...}の実現 def slice(*slicers) if block_given? slicer = yield(Slicer.new(self)) slicers << slicer.evaluate end # ... end end
ブロック内で条件式¶ ↑
# rouge ruby class Arrow::Slicer def initialize(table) @table = table end # slicer.priceの実現 def method_missing(name, *args, &block) ColumnCondition.new(@table[name]) end end
ブロック内で条件式¶ ↑
# rouge ruby class ColumnCondition < Condition def initialize(column) @column = column end # slicer.price >= 500の実現 def >=(value) GreaterEqualCondition.new(@column, value) end end
ブロック内で条件式¶ ↑
# rouge ruby class GreaterEqualCondition < Condition def initialize(column, value) @column = column @value = value end # slicer.price >= 500を評価 def evaluate # ホントはC++で実装 @column.collect {|value| value >= @value} end end
ブロック内で条件式¶ ↑
# rouge ruby class Condition # (slicer.price >= 500) & (...)の実現 def &(condition) AndCondition.new(self, condition) end end
条件式のポイント¶ ↑
* 遅延評価 * ❌各要素毎にブロックを評価 * ブロックは一回だけ評価 * ブロックで指定した条件は\n コンパイルしてC++実装で実行
Red Arrowと外の世界¶ ↑
* ハブになるといいかも! * 各種データと変換可能に * 各種オブジェクトと変換可能に * Ruby間の連携を推進 * 今は互換性がないライブラリー\n 🠊連携できるように!
データ変換¶ ↑
* (({Arrow::Table.load})) * データの読み込み * (({Arrow::Table#save})) * データの書き出し
データ変換例¶ ↑
# rouge ruby # CSVを読み込んで table = Arrow::Table.load("a.csv") # Arrowで保存 table.save("a.arrow") # Parquetで保存 table.save("a.parquet")
CSVの読み込み¶ ↑
* CSVは広く使われている * CSVなデータを簡単に使えると捗る * 難しいところ * データ定義が緩い\n 例:カラムの型情報がない
Red Arrowでの読み込み¶ ↑
* 2パスで処理 (1) 全部処理して各カラムの型を推定 (2) 推定した型でArrowのデータに変換 * 😅時間がかかる * 読み込んだデータを別の形式に変換して再利用する使い方を想定\n だから、まぁ、いいかなぁって
型の推定¶ ↑
# rouge ruby candidate = nil column.each do |value| case value when nil; next # ignore when "true", "false", true, false; c = :boolean when Integer; c = :integer # ... else; c = :string # わからなかったら文字列 end candidate ||= c candidate = :string if candidate != c # 混ざったら文字列 break if candidate == :string # 文字列なら終わり end candidate || :string # わからなかったら文字列
オブジェクト変換¶ ↑
* Numo::NArray, NMatrix * 既存の多次元配列オブジェクト * PyCall経由でPyArrow * Rubyでデータ作成🠊Pythonで処理 * GDK Pixbuf * 画像オブジェクト
オブジェクト変換例1¶ ↑
# rouge ruby # PNG画像を読み込み pixbuf = GdkPixbuf::Pixbuf.new(file: "a.png") # Arrow経由でNumo::NArrayに変換 narray = pixbuf.to_arrow.to_narray # 不透明に(アルファ値(透明度)を0xffに) narray[true, true, 3] = 0xff # Arrow経由でGdkPixbuf::Pixbufに変換 no_alpha_pixbuf = narray.to_arrow.to_pixbuf # GIF画像として保存 no_alpha_pixbuf.save(filename: "a-no-alpha.gif")
Pixbuf→Arrow¶ ↑
# rouge ruby class GdkPixbuf::Pixbuf def to_arrow bytes = read_pixel_bytes # ピクセル値 buffer = Arrow::Buffer.new(bytes) # 高さ、幅、チャンネル数の3次元配列 # チャンネル数:RGBAだと4チャンネル shape = [height, width, n_channels] # バイト列なのでUInt8 Arrow::Tensor.new(Arrow::UInt8DataType.new, buffer, shape) end end
ゼロコピーの実現¶ ↑
* ゼロコピー * 同じメモリー領域を参照し、\n コピーせずに同じデータを利用 * 速い!!! * データ * バイト列へのポインター * Rubyでは(({RSTRING_PTR(string)}))
(({String}))とゼロコピー¶ ↑
# rouge c /* pointerの内容をコピー */ rb_str_new(pointer, size); /* pointerの内容を参照:ゼロコピー */ rb_str_new_static(pointer, size);
(({String}))とゼロコピーとGC¶ ↑
# rouge c arrow_data = /* ... */; /* ゼロコピー */ rb_str_new_static(arrow_data, size); /* arrow_dataはいつ、だれが開放? */
Rubyでゼロコピー¶ ↑
* ❌(({rb_str_new_static()}))だけ * メモリー管理できない * メモリー管理する何かが必要
Red Arrowでゼロコピー¶ ↑
* GBytesを利用 * GBytes * GLib提供のバイト列オブジェクト * リファレンスカウントあり * Rubyだと(({GLib::Bytes}))
(({GLib::Bytes#to_s}))¶ ↑
# rouge c static VALUE rgbytes_to_s(VALUE self) { GBytes *bytes = RVAL2BOXED(self, G_TYPE_BYTES); gsize size; gconstpointer data = g_bytes_get_data(bytes, &size); /* ゼロコピーでASCII-8BITな文字列を生成 */ VALUE rb_data = rb_enc_str_new_static( data, size, rb_ascii8bit_encoding()); rb_iv_set(rb_data, "@bytes", self); /* GC対策 */ return rb_data; }
(({GLib::Bytes#initialize}))¶ ↑
# rouge c static VALUE rgbytes_initialize(VALUE self, VALUE rb_data) { const char *pointer = RSTRING_PTR(rb_data); long size = RSTRING_LEN(rb_data); GBytes *bytes; if (RB_OBJ_FROZEN(rb_data)) { /* ゼロコピー */ bytes = g_bytes_new_static(pointer, size); rb_iv_set(self, "source", rb_data); /* GC対策 */ } else { /* コピー */ bytes = g_bytes_new(pointer, size); } G_INITIALIZE(self, bytes); return Qnil; }
GBytesはすでに使っていた¶ ↑
# rouge ruby class GdkPixbuf::Pixbuf def to_arrow # ピクセル値はGLib::Bytes bytes = read_pixel_bytes buffer = Arrow::Buffer.new(bytes) shape = [height, width, n_channels] Arrow::Tensor.new(Arrow::UInt8DataType.new, buffer, shape) end end
オブジェクト変換例2¶ ↑
# rouge ruby # RelationをArrow形式に users = User .where("age >= ?", 20) .to_arrow
Relation→Arrow¶ ↑
# rouge ruby module ArrowActiveRecord::Arrowable def to_arrow(batch_size: 10000) in_batches(of: batch_size).each do |relation| column_values_set = relation.pluck(*column_names).transpose data_types.each_with_index do |data_type, i| column_values = column_values_set[i] arrow_batches[i] << build_arrow_array(column_values, data_type) end end # ... Arrow::Table.new(...) end end
Red Chainer¶ ↑
(('tag:large')) ChainerのRuby移植
スライドプロパティー¶ ↑
: as-large-as-possible
false
Chainer¶ ↑
* 深層学習フレームワーク * Pythonのみで実装 * 移植しやすい * Pythonのみで実装されているから
Red Chainerサンプル¶ ↑
# rouge ruby model = Chainer::Links::Model::Classifier.new(MLP.new(args[:unit], 10)) optimizer = Chainer::Optimizers::Adam.new optimizer.setup(model) train, test = Chainer::Datasets::Mnist.get_mnist train_iter = Chainer::Iterators::SerialIterator.new(train, args[:batchsize]) test_iter = Chainer::Iterators::SerialIterator.new(test, args[:batchsize], repeat: false, shuffle: false) # ...
(('tag:center')) 横に長い
Chainerのサンプル¶ ↑
# rouge python import chainer import chainer.links as L model = L.Classifier(MLP(args.unit, 10)) optimizer = chainer.optimizers.Adam() optimizer.setup(model) train, test = chainer.datasets.get_mnist() train_iter = chainer.iterators.SerialIterator(train, args.batchsize) test_iter = chainer.iterators.SerialIterator(test, args.batchsize, repeat=False, shuffle=False)
(('tag:center')) こっちも横に長い
Pythonでの短く仕方¶ ↑
# rouge python # 通常 import chainer model = chainer.links.Classifier(MLP(args.unit, 10)) # Lでショートカット import chainer.links as L model = L.Classifier(MLP(args.unit, 10))
Rubyでの短く仕方¶ ↑
# rouge ruby # 通常 model = Chainer::Links::Model::Classifier.new(...) # Lでショートカット L = Chainer::Links model = L::Model::Classifier.new(...)
PythonとRubyの違い¶ ↑
* Python * ファイル内でだけ(({L}))が有効 * ファイル単位でネームスペース * Ruby * グローバルに(({L}))が有効 * 微妙!
Rubyらしく短く¶ ↑
# rouge ruby # Rubyのネームスペースの仕組みはモジュール Module.new do include Chainer::Links model = Model::Classifier.new(...) end
開発したくなった?¶ ↑
* 楽しそう! * 一緒に開発しようぜ! * やっぱRubyでデータ扱いたい! * 一緒に開発しようぜ! * レベルアップしたい! * 一緒に開発しようぜ!
レベルアップへの道¶ ↑
* 😅NG集をたくさん覚える * 😀よいコードにたくさん触れる
よいコード¶ ↑
(('tag:large')) Red Data Toolsにn たくさんある!
スライドプロパティー¶ ↑
: as-large-as-possible
false
一緒に開発¶ ↑
* 🌏どこからでもオンラインで * GitHubとGitter(チャット)を使用 * ➕東京は月一オフラインで * 🔍「OSS Gate Red Data Tools」
Join Red Data Tools!¶ ↑
* Webサイト * (('tag:x-small'))((<URL:https://red-data-tools.github.io/ja/>)) * Gitter(チャット) * (('tag:x-small'))((<URL:https://gitter.im/red-data-tools/ja>)) * GitHub * (('tag:x-small'))((<URL:https://github.com/red-data-tools/>))