Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Rustで扱える機械学習関連のクレート2021

- はじめに -

本記事では、Rustで扱える機械学習関連クレートをまとめる。

普段Python機械学習プロジェクトを遂行する人がRustに移行する事を想定して書くメモ書きになるが、もしかすると長らくRustでMLをやっていた人と視点の違いがあるかもしれない。


追記:2021/02/24

repositoryにしました。こちらを随時更新します
github.com


追記;2021/07/26

GitHub Pagesでウェブサイトにしました
vaaaaanquish.github.io

- 全体感 -

Rustで書かれた(もしくはwrapされた)クレートは、かなり充実しつつある段階。

特に流行しているNeural NetworkないしDeep Learning関連のクレートは更新が盛んである。TensorFlowやPyTorchのRust bindingsもあれば、ゼロからRustで全て書こうというプロジェクトもある。

Numpy、Pandasを目指すプロジェクトも既に存在しているし、元々C/C++で書かれているライブラリであれば、rust-bindgenを使ってRust bindingを簡単に作れるようになってきている。
GitHub - rust-lang/rust-bindgen: Automatically generates Rust FFI bindings to C (and some C++) libraries.

古典的な画像処理やテキスト処理(例えばshift特徴量が欲しいだとかTF-IDFを計算したいだとか)で色々と物足りないクレートを使うことになる場合もありそうだが、画像はimage-rsという大きなクレートが存在し入出力を握っているし、形態素解析器などのクレートも多いので、アルゴリズム部分を自分でガッと書けば良いだけであると言えそう。
 
 

「物足りないクレート」と言ったが、2016~2018年から更新のないクレート、C++ライブラリのリンクが上手くいっていないクレート、Cargo.tomlだけのクレート(crates.ioにアップロードされ名前空間汚染になっている)などがあるという話で、その辺りを踏まえるとまだ(Pythonに比べたら)とっつきにくさがある。

Pythonであれば様々な機械学習モデルへの入力の多くをNumpyを使った行列で記述するが、Rustでの同等のプロジェクトではndarrayがあるものの安定し始めたのはちょうど1年前くらいからで、クレートによって入出力がrustのvectorだったりndarrayだったり、また別のnalgebraというクレートを使っている場合もあるといった状態である。

あとは機械学習の実験時に必要な物事をどうするかという問題も殆ど定まっていない。パラメータ管理はJSONを使うのか、設定時はRustらしくメソッドチェーンを使うのか、どう結果を保存するのか、パイプラインを作っていけるのか、など解決していない(デファクトスタンダードが定まっていない)課題は山積みである。

まあでも似たような問題はJulia langが流行り始めた頃にもあったように記憶していて、参入者が多くなるにつれて、自然とデファクトスタンダードは決まっていくだろう、と感じている。


ここでは、個人的にこれが残っていくんじゃないかと考えているものを優先的に書く。

 

- 機械学習足回り関連のクレート -

jupyterとかnumpy、pandasとか画像、テキスト処理などの機械学習前処理関連のもの
 

Jupyter Notebook

Python機械学習関連の物を作る人の多くがJupyterを使っているだろう(と思っている)。googleプロジェクトの配下にあるEvcxrが便利。
github.com
Notebookカーネル、REPLがあるので、普段Rustの小さな挙動を確認したい時はこれを起動するかシェルから叩いている。私としてはかなり開発速度が上がったツールの1つ。

類似のものとしてはrustdefとかを見てみたが、Evcxrの方が使い勝手がPythonカーネルに近い。


matplotlibやseabornとまではいかないが、vectorであればグラフ描画にplottersのjupyter-integrationが使いやすいなと思って入れてはいるものの、データ分析、EDAにおいてはやはり型にうるさくないPythonがやりやすいので結局Pythonカーネルを起動してる。
GitHub - 38/plotters: A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely 🦀 📈🚀

plotlyのRust bindingsもあって、Pythonくらい柔軟になればあるいはとも思っている。EvcxrをSupportしたのが6ヶ月前にReleaseされた0.6.0(現行最新バージョン)であるのでこれからという感じ。
GitHub - igiagkiozis/plotly: Plotly for Rust

 

Numpy/Scipy

ndarrayというクレートnalgebraというクレートが2大巨頭で争っている。
redditの議論: ndarray vs nalgebra : rust

解決したい課題がnalgebraは線形代数特化な様相もあって、Pythonのように動的によしなに行列をスライスしたり変形させる機構ではなく、コンパイル時に行列の大きさを推定しメモリを確保してそこを使う実装になっている。ピュアRustという事もあるし、ndarrayのような柔軟さが欲しい場合はRustを使う意義があまりないと私は思っている。機械学習で扱う行列演算で曖昧な処理をしてバグを生むことも多いので、機械学習で使う側面から見てもnalgebraが幅をきかせていくと思っているがndarrayという名前に勝てず皆使っているのはndarrayという感じ。

github.com
github.com


(ちなみに私が作っているライブラリでは決めかねてvec![]を使っているごめんなさい)

Pandas

polars一択だと思う。polarsはpythonバインディングもあり、pandasより早い事を謳うライブラリの1つでもある。
github.com
applyやgroupby、aggを使う限りでは、殆どpandasと遜色なく扱える。

参考になる: Rustのデータフレームcrateのpolarsとpandasの比較
 

Queryを扱うという点ではarrowを使う事でデータの処理が行えるが、オンメモリでpythonのDataframeのようにとは少し違ってくる。
https://github.com/apache/arrow/tree/master/rustgithub.com


他にblack-jackというめっちゃかっこいい名前のクレートやrust-dataframeutahといったpandasを意識したクレートがあるが、開発は滞っている。
 

画像処理

image-rs配下のプロジェクトが一番の選択肢になると思う。
github.com
ImageBufferがfrom_vec, to_vecを持っているのでvectorとのやり取りも難しくない。特徴量抽出など、少し複雑な処理を行う場合はimageprocに実装されていってる感じなので、こちらを見ていくと良さそう。
GitHub - image-rs/imageproc: Image processing operations
 

前述のnalgebraを使う前提であれば、cgmathでGPUSIMD最適化などのオプションを付けて多くの線形な処理が行えるので、画像処理のみを目的にするならこちらも選択肢に入る。
github.com
近年の画像処理 * 機械学習観点だとここまで必要になる事は、そう多くはないのかなと思ったりもする。リゾルバを書いて何か解決したい場合とかだろうか。
 

次点でopencv-rsustというクレートがあるのだが、私は結局opencvへのリンクを上手くやってbuildして動かした所で力尽きて終わってしまった。OpenCVインストールバトルやcv::Matの扱いに慣れている場合は選択肢に入るかもしれない。ndarray-imageというndarrayで扱おうというrust-cvなるプロジェクトもあるが、開発が盛んとは言えない(AkazeやBrute forceみたいな古典的な画像処理アルゴリズムを熱心に実装しているのはrust-cv)。

形態素解析/tokenize

lindera になりそう。
github.com
mecabやneologdのような既存の資産も扱えるし、ピュアRustである点も含めて扱いやすい

linderaメンテナのブログ:Rust初心者がRust製の日本語形態素解析器の開発を引き継いでみた - Qiita
私も過去に使ったブログを書きサンプル実装を公開している:Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類 - Stimulator

その他には、sudachi.rsyoinawabi のような実装もあるがメンテは止まっている様子である。

 
英語のTokenzeであればPythonでもおなじみのhuggingfaceのtokenizersがRust実装なのでそのまま扱う事ができる。
github.com

 

- scikit-learn的なやつ -

scikit-learnみたいに色んなアルゴリズムが入ったクレートは有象無象にある。
大体どれも以下のアルゴリズムはサポートしている。

  • Linear Regression
  • Logistic Regression
  • K-Means Clustering
  • Neural Networks
  • Gaussian Process Regression
  • Support Vector Machines
  • Gaussian Mixture Models
  • Naive Bayes Classifiers
  • DBSCAN
  • k-Nearest Neighbor Classifiers
  • Principal Component Analysis
  • Decision Tree
  • Support Vector Machines
  • Naive Bayes
  • Elastic Net

各ライブラリと特徴比較

流石に全部使って見るという事ができていない上に、更新が止まっているものも多いので、最終commit日と一緒にリストアップする

  • rusty-machine (Star: 1.1k, updated: 2020/2/15)
    • 一番よく記事等を見るやつ。最終更新は1年前だが、アルゴリズムよりsrc/analysis配下のConfusion Matrix、Cross Varidation、Accuracy、F1 Score、MSEの実装が参考になるのでよく見に行く。iris等簡易データセットの読み込みもあるがデータだけなら下記のlinfa datasetが良いと思う。
    • Generalized Linear Modelを広くサポートしているのはちょっと特徴的
  • linfra (Star: 658, updated: 2021/1/21)
    • InterfaceもPythonっぽいし、完全にsklearn意識で開発も盛りあがっていてSponsorもいる。特にsklearnと違って各アルゴリズム毎にクレート化されてるのが嬉しい。
    • 上記以外にGaussian Mixture Model ClusteringやAgglomerative Hierarchical Clustering、Elastic Net、ICAをサポートしている
  • SmartCore (Star: 66, updated: 2021/1/22)
  • rustlearn (Star: 470, updated: 2020/6/21)
    • 古いライブラリだがPure Rustでinterfaceもわかりやすいので実装が参考になる
    • レコメンドでよく使うfactorization machinesや多くのmetric、k-fold cross-validation、shuffle splitのようにかなりピンポイントで重要なアルゴリズムを採用している

sckit-learnの立ち位置になっていきそうなのは、Sponsorもついているlinfaか開発者の勢いがあるsmartcoreというイメージ。
github.com
github.com

 

- Gradient Boosting -

主に使われているXGBoost、LightGBM、CatboostにRust bindingsが存在するが、現状modelのtrainまで出来るのはXGBoostとLightGBMのみ。

XGBoost

公式のXGBoostのC++実装をbindgenでbuildしてwrapした実装がある。
github.com
wrapperなので、PythonのXGBoostと同じイメージで使える。開発はほぼ止まっていて、submoduleとして使っているXGBoostのバージョンが少し古かったり、GPU等のSupportがないのがネックではある。

XGBoostで学習したデータを読み込んで推論できる、gbtree実装を使うという手もあるが、まだ上記のwrapperの方がとっつきやすいとは思う。
github.com
 

LightGBM

手前味噌ではあるが私がwrapperを書いている。上記のXGBoostと同様、C++実装をcmakeしてbindgenでbuildしてwrapしたもの。
github.com

parameter configをserde_jsonにしていたり入出力がvectorだったりして本当にこれで良いのか議論したいし、まだサポートしていないc_apiWindowsGPUを作る必要もあるので皆さんのcommitを待っている。

 

CatBoost

Catboostは公式に「一応」Rust bindingsが入っている。
以下のPRを見ての通り、適当なレビューはされておらずドキュメントはない…。
github.com
実装も怪しいが、私が手元で動かしてみた所、ライブラリへのリンクが上手く通っておらずそもそも動かなかった。

XGBoostやLightGBMと同様の方法でbindingsを作れば良いという話でもあるのだが、厄介な所として、Makefileではなくyandexの社内のモノリスで動いていたyamakeというビルドシステムを使ってビルドしている(普通にbuildすると謎のバイナリをダウンロードしてきて動かされる)。
それはやめようよというissueが立ち、catboost/makeディレクトリにMakefileが用意されるようになったが、このMakefileもyandex社内のArcadiaというシステムがジェネレートしたものでかなりヤンチャ(OSごとにファイルが分かれているにも関わらずドキュメントがそれに追い付いてなかったりもする)。
Ya script sources · Issue #131 · catboost/catboost · GitHub

masterが動かなかったのでコードを読む限りだが、現状は学習済みのバイナリを読み込んでpredictする形式しかサポートしていない。これはtrain周りのc_apiがwrapされていないからで、その部分を自前で書く必要もある。

一応私はここ最近Makefileとbuild.rsを作ることに挑戦していて、とりあえずmakeが通ってlibcatboostが生成される所まで来たが、心が折れるかもしれない(折れそうだったのでこの記事を書いて気を紛らわせている)。

 

- Deep Neaural Network -


現状はPytorch、TensorFlowの公式bindingsの2強の状態と言える。

系譜を以下の記事から引用する。

私の知る限りではRust界隈のディープラーニングフレームワーク事情は以下のとおりです。

primitiv-rustでディープラーニングする - Qiita

この記事で紹介されているprimitiv-rustdynet-rsも更新は止まっている。

Tensorflow/PyTorch

公式Bindingsが使える。

github.com

github.com

私はTensorFlow 2.xやKerasが入ってAPIがもう追えなくなったのでPyTorchを使ってるが、tch-rsは不満なく使えている。pretrained modelによる学習もすぐ始められるし、後述するようなBERT、Transformerの実装もあるのでほとんどPythonと遜色ない。GPUにも載る(私は試せていないが)らしい。
 

BERT

最近Transformerの実装の参考に触り始めたtch-rsをベースにしたrust-bertというクレートがあり、難しくなく扱える。
github.com

日本語のPretrainモデルをコンバートしてくる必要があるので、そこが少し面倒だが、みんな大好きHugging Faceのライブラリ群がかなり巻き取ってくれているので、Pythonで書いている時とあまり変わらない印象。
 

- Natural Language Processing -

TF-IDF

ほとんど簡易な実装だが、下記がある。
github.com

sckit-learnのようにTF-IDF Vectorizerみたいな使い方は、issueを見る限りあまりサポートされなさそうなので、その場合は自前で書けば良さそう。

fasttext

公式のFastTextの実装をbindgenでbuildしてwrapしたものがある。

github.com

私も過去に使ったブログを書きサンプル実装を公開している:Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類 - Stimulator
開発としては止まっていて、submoduleとして使っているFastTextのバージョンが遅れているのがネック。
 

- Recommendation -

Collaborative Filtering/Matrix Factorization

協調フィルタリングで多分一番実装がまともなものがquackinになる。もう4年更新がないが、実装自体がシンプルなのでフォークすれば良さそう。
github.com

他にもrecommenderが一応クレートとして見られるけど、ここまでなら自前で作っても良い。
より古典的なVowpalWabbitのRustバインディングも選択肢に入る。
github.com

 

Matrix Factorizationであれば、先述のrustlearnを使うのが良さそう。

non negative matrix factorizationの実装のクレートがあるが、試してみた所精度が出なかったので難しいところ。
GitHub - snd/onmf: fast rust implementation of online nonnegative matrix factorization as laid out in the paper "detect and track latent factors with online nonnegative matrix factorization"

- Information Retrieval -

Apache Solr入門や先の形態素解析のLinderaをメンテしているMinoru OSUKA(@mosuka)さんが作っているgRPCで通信して検索するbayardが全文検索エンジンとしての出来が良い。
github.com
(というよりまともに動くのはこれくらいな気がする)

作者の解説記事:Rust初心者がRustで全文検索サーバを作ってみた - Qiita
初心者が形態素解析器と全文検索エンジンを作っている。謎。
 

フロントでは、WebAssemblyでメモリ効率良く設計されたTinysearchがかなり気になる(使えていない)
github.com
フロントエンド側で検索できて、gzipにすると51KBしかないとか。
公式のブログがかなり参考になる:A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly | Matthias Endler
作者はTrivagoの検索のバックエンドを作っている人らしい。

WebAssemblyでフロントで検索するのは熱く、StripeのengineerからもRustでwasmを介したStrokが出てる。
github.com
 

より一般的なものであれば、Elasticsearchが公式のクライアントとしてElasticsearch-rsを出しているので、裏側にESが既に立っているとかならこれで良さそう。
GitHub - elastic/elasticsearch-rs: Official Elasticsearch Rust Client

 

近傍探索ではFaissのc_apiをwrapしたRust bindingsがある。使い勝手はほぼPythonと同じ。

github.com

他の選択肢としては、HNSWやHNSW graphsをベースにしたgranneがあるので要検討。Pure Rustな所がかなり嬉しいと思う。
github.com

github.com

もうちょっと古典的なものだとVP木をつかったvpsearchがある。kd-treeの実装もあるのでkd-tree版も作れなくはなさそう。

- Reinforcement Learning -

強化学習であれば、rurelというクレートがあるが、流石に私も触りきれていない。
github.com

gymのRust bindings作ってる人もいる。すごい。まだ触れてないけど使えるかも。
github.com

 

おわりに

結局の所「wrapするだけならPython(ないしCython)が便利じゃない?」という所とどう折り合いつけるのかというのはある。

ただ、分散処理するほどではない大規模データ、型のないPythonでは扱いの難しいデータ、wasmによるフロントエンドでの高速処理など、今まで機械学習の社会実装で苦労していたニッチな所に刺さると思うので良いとも思う。
 

他にもこれオススメだよってやつあったら触りに行くので、はてブかツイートしてください。

 f:id:vaaaaaanquish:20210123234057p:plain:w0

【参考】

crates.ioでMachine Learning等で調べた。


 

3ヶ月くらいフロントエンドやったのでやったこと一旦まとめ

- はじめに -

9月くらいから趣味でフロントエンド周りをやっていたので、その勉強過程のまとめ。

何が良かった悪かったとか、こうすればよかったとか、所感とか。

 

- 前提 -

前提として9月頭くらいの私のフロントエンドに対する理解と技術的な知識はこんな感じ。

これくらいのレベルからこうやって学んでいったぞという記録になる。


 

- どんな感じで進めたか -

4つWebアプリを作った。3つは友人の手伝い、1つは以下のWebAssenblyを組み合わせたWebアプリで、これは1人で作成した。

vaaaaaanquish.hatenablog.com


  

最初の開発

最初、ElasticSearchを使ったデータ検索系のサービスを趣味開発チームのメンバーが作ろうとしていたので、並び替えやランキングに機械学習が必要だろうという事でバックエンドのElasticCloudや機械学習モデル作成を手伝った。その過程で検索フォームやサジェストが必要になって既存のReact部分を触らせてもらった。

この時は、そもそもComponentという概念が全く理解できておらず、なんかclass作ってStateとPropsでやり取りするらしいくらいの感じでコードを書いていた。友人にFluxかReduxを使ってと言われたので、オススメの本を聞いたら「本でもブログでもなく公式のチュートリアルを見てやれ」「本ならとにかく新しい本を買え」との事だった。

今にして思えば、このアドバイスは神だった。

 
最初はこれを2日程かけてやった。
ja.reactjs.org
react-redux.js.org
React Devtoolsが便利だとか、Reduxが何故必要なのかがよく分かった。
他のブログ記事の野良チュートリアル的なのも見たは見たけど、公式が最新のバージョンでコードを上からコピペしていけば動くのでどこよりも良い。アドバイス通りだ、当たり前だけど。

 
非同期処理なんかもjQueryで適当に書いていた頃とは勝手が違っていて苦労したが、何故かTwitterで回答が得れたりした。


mizchiさんただのOSSタダ乗りおじさんだと思ってたごめんね。

 
頭のおかしい友人は「前時代から入ってきた人はまずはこれでしょ」と言ってAtomic Designを薦めて来たが、この時はマジでよく分からなかった。
atomicdesign.bradfrost.com

後述するが、後々にWebアプリ2,3個作った辺りで「あーなるほど」と思う事があった。


実際に作ってみてから読むと、良いコンポーネント設計が出来るようになる感じはするので時間が空いたら、開発手法を学ぶ的な意味合いでAtomic Designはオススメできる。本家が難しければググれば解釈がいくつか出てくるのでそちらでも。

 
その辺りで、もうちょっと体系的に学べないかと思って調べて、アドバイス通り「新しめの本」ということで以下を読んでみたが、これもなかなか良かった。

Web上のチュートリアルと違って動かせるコードは少ないが、Vue、Angular、Reactの違いとか、何となく書いていたBabel、webpackが何をやっているか、ReduxやFulxがなぜ必要かが体系的に書いてあって、本当に助かった。後半ユニットテストや実際のサービス開発フロー、Sentryやスクラムの話が出てくるがあまり読めてないのでまた読みたい。

これ読んだ辺りから、Qiitaやはてな、Zennで流れてくるフロントエンドエンジニアの間で話題の記事をかなり難なく読めるようになったので、チュートリアルとこの本は結構効いた。


 
理解できてきて記事のシェアが増えてきた

他にもこの辺りでのReduxとの出会い、TypeScriptの学びは大きかった。

Reduxの機構を見た時、「本当にこんな壮大な仕組み必要か?」と思ったが、実際使ってみると簡単で便利だった。後にReact Hooksで多くの機能が代替できる事を知って移行していったが、状態というものが何なのか意識できるようになったのはRedux使った後からだったように思う。最初からHooksでも良い部分が多いけど、Componentやnode.js周りの開発がどれだけ良いか体験できるのも良い。例えばFluxなプロジェクトをReduxに書き換える、Hooksに書き換えることを経験したが、状態管理が1ライブラリで切り離されているので、移行の労力がかなり小さかった。これがいわゆる近年のnode.js開発の良さの1つなのだろう。

TypeScriptへの移行もやった。この辺りにもnode.js開発プラクティスの恩恵があるんだなと感じる体験で、Componentで分かれているのでComponentごとに1つ1つ変えていけば良いし、何よりTypes書けば推論してくれる便利さ、ビルド時のチェック、エディタの補完の進歩が最高だった。以下の本も読んだら良かった。

実践TypeScript

実践TypeScript

フォロワーが書いてるからと思って読んだが、この書籍でNext.jsと組み合わせた時の強力さも知れた。書いてある内容がちょっと古いけど、筆者が追加で最新分に対応した解説しているのでこちらも見ると良い。
Nuxt.js TypeScript - 実践TypeScript アップデート - - Qiita


 
最初のアプリの小話だが、友人はElasticCloudのオススメ設定みたいなので進めていて全盛りプランでお金掛かりまくる感じになっていてヤバと思った。流石フルマネージド。結局まるっと構成を変えてインデックスも機械学習モデル入れやすいように貼り直したりして大変だった。


  

TypeScriptとNext.jsを使った開発

次のWebサービスも辞書のようなサービスを手伝う形だったが、途中まで書かれたサンプルがwebpackだけ使ったCSRだったので「いやこれTwitterでシェアされたりすること考えるとSSR必要じゃないか」という話になってNext.jsをやった。

Google検索やTwitterに出たかったらSSRだNextをやれと最初に知りたかった気もちょっとした。

 
この辺りまでwebpack.config.jsをダラダラ書きまくっていたが、どうやらNext.jsではpackage.json、tsconfig.json(あるいは.babelrc)の簡易なフォーマットを書けば良く、ルーティングもディレクトリ、ファイル名でよしなにできる。Routerの分岐をコツコツ書かなくて良い(すごい)。静的なファイルの配信もクライアントサイドのレンダリングも簡単で、ほとんど何も考えずComponentを書いていけば開発できる事がわかった。すごい。

(最初はなんでGoogleクローラやTwitterのOGPなんかのために頭良い人達がSSRだの何とかRだのに振り回されてんのと思った正直)

 
これも出来るだけ公式のチュートリアルをなぞった方が良い。
nextjs.org

野良のやってみた、ベストプラクティスみたいな記事は半年以内くらいじゃないと若干古かったりDeprecatedだったりする。


コンポーネントやライブラリの使い方がちょっと違うくらいならいいが、特にディレクトリ構成が変わる系(publicとかsrcとか)は後から変更するのが大変な場合もあるのでちゃんと最新バージョンの情報を見たほうが良い(大変だった)。


この辺りでMaterial-UI: A popular React UI frameworkとかのドキュメントをひたすら上から試したり、About splash-screens - AppscopeFavicon Generator for perfect icons on all browsersfaviconやスマフォ向けの画像作ったり、Google Analytics、OGPなんかを一通りやった。

f:id:vaaaaaanquish:20201227210125p:plain:w500
material-uiのドキュメント全部読んだ時の

Tailwind CSSSemantic UIも比較のために触った(Web上の記事も読んだがあんまり良い悪いの比較が理解出来ずmaterial-uiで良いんじゃないという気がするが理解が足りていない気もする)。


 

アプリ手伝いから自分のアプリ開発まで

3つ目はNuxt.jsなアプリをちょっとだけ手伝った。あんまりNextとやってる事は変わらないと感じたけど、逆に変わらないのがすごいなと思った。外枠変わっても書いてるTypeScriptのコード変わらないのすごい。まあでもつまりNext.jsとTypeScriptができれば結構いろんな事に対応できそうだという事もここでわかった。

 
この辺りで、WebAssenbly nightというイベントを聴講して、「あ、こりゃ面白いな」と思って自分のサービス作ってみようとなった。
emsn.connpass.com

 
Rustで機械学習がどれくらいできるか試して
vaaaaaanquish.hatenablog.com
その機能がwasm-packやEmscriptenでバイナリにしてNext.jsで配信できるか試して


できてた


前のブログに書いた通り、wasm周りは結構大変だったが、技術的に面白いものが何だかんだで1万人強に使ってもらえていて、楽しい気持ちになった。


この辺りは本心で、かなり大変な上に「機械学習エンジニアにとってwasmでサービス展開したいモチベーションが薄いので量子化ホスティングまでを考える人が少なくPythonAPIがソリューションになる」「逆にフロントエンドエンジニアやバックエンドエンジニアが機械学習モデルの量子化をやるのかと言うとそれはそれでとなる」という状況で、一部のエンジニアを除いて多く人にとってはちょっとしんどい気もした。

こういうWebサービスやりたいよという会社があったら呼んで下さい。

 
御託はさておき、GAEやFirebaseであまり考えずにNext.jsでWebサービス展開ができるようになった。


 

- できてないこと -

趣味で3ヶ月やってみたものの、まだ出来てない事、分かってない事がいくつかある。

  • ツールチェインのベストプラクティス

先に挙げた「Material UI、Tailwind CSS、Semantic UI」もそうだし、「Express、Parcel、Deno、Gatsby...ないし任意の技術は良いぞ」というのが飛び交っていてまだまだ試す事が多い

  • Babelが何をやってるか

これ結構わからない人多いと思うけど私だけなのかな。使い方は出てくるけど、ES構文とは何なのかとかを知らないと把握できなさそうだなと思う。
こういう所が、こんなツイートにも繋がっている。

これマジでわからん。ESLintどの粒度で書くのが正解なのか、気付いたらNext.js含む回帰テストみたいになってしまう。これを読めみたいなやつないのかな。

わからん。デザインツールから出てきたscssを適応するのも、既存のCSSを改修するのも1日以上かかったし、css-loaderやscss-loaderがCSS Moduleになったからなんぼのもんじゃいって気持ち。PostCSSになっても適切にdisplayやpositionを適切に一発で設定できたことなし。これを使えみたいなやつないのかな。

  • パフォーマンス最適化

この書き方がどういう理由でパフォーマンスが良い、高速であるというのをあまり把握できていない。これを読めみたいなやつないのかな。

  • 複数人での継続的な開発

アトミックデザインなりのnode.jsなり良いところはこの辺にあるんだろうと体感していて、複数人が手を入れても、モジュールのバージョンアップデートも変更もこりゃやりやすいだろうと思うんだけど如何せん仕事じゃないので機会が訪れなさそう。


 

- 所感 -

たしかにnode.js開発しやすい。jQueryをゴリゴリ書いていた時を思うと、Releaseまでの速度も改修のしやすさも、他人のコードの読みやすさも段違いに良い。Railsとの比較が近年多いけど、私はまだRails書きまくってるわけじゃないから何か機能の比較は難しい。それでも大きなフレームワークとは違った、色んな開発の概念を学べてよかったので色んな人がやると良いと思う。

情報の古さみたいなのがヤバいので公式を見に行く精神を忘れるとしんどい。ライブラリの開発がやりやすいこと、早いことが起因しているのだろうけど、ついていけている開発者も少ないのではとちょっと思った。

自分の作ったサービスが使われるのを見るのは楽しい。
機械学習エンジニアの悩みどころの1つに「小さいサービスではやることがない問題」というのがあって、最初から機械学習を前提としていたりしない限り、データやユーザが小さかったり需要がないために(概ね習熟した問題に対して改善フェーズじゃなければ)機械学習モデリングは実は簡単に終わってしまって分析屋かPdMになっていくという事象はよくあるのだが、私は開発に比べたらあんまりという気持ちなので、もうちょっと開発の範囲を広げる方向で何か考えていた。バックエンドやフロントエンドをやってみるのは楽しかったので今後も継続したい。

Rustも書けるWebサービスのアイデアもっと出したい。


 

- おわりに -

あんまりこういうまとめ書く気はなかったけど、最近「初心者のうちに初心者な記事を書かないと、初心者の読者と乖離した記事しか書けない」みたいなツイートを見て「それもそうだ」と思ったので書いてみた(元ツイート見つけられなかったごめんなさい)。


結構楽しかったので普段フロントエンド触ってない皆さんも年末のお供にどうぞ。



 

- 追記 -

2020/12/28 

分からなかった事が分かりそうなgistが作られてました。

めちゃくちゃありがたい。やっていきます。
 

WebAssemblyで機械学習Webアプリ「俺か俺以外か」をつくった

- はじめに -

文章がローランド(@roland_0fficial)様っぽいか判定するサービスをつくった。

学習済みモデルをダウンロードし、WebAssemblyで形態素解析機械学習モデルによる判定を全てブラウザ上で処理する。

この記事は、そこに至るまでメモ。

- 技術的な概要 -

何が面白いのか簡易図

f:id:vaaaaaanquish:20201226031341p:plain:w500
なんか適当な図

学習済みの機械学習モデルをダウンロードして、手元のブラウザ上で動くjavascriptだけで、テキストの処理や判定をするというもの。WebAssembly(以下wasm)自体はまだ出始めの技術ではあるものの、面白い試みが近年増えて来ているので興味もあって作った。

- データの収集 -

我らがローランド様のTwitterInstagramのテキスト部分をクロールした。

twitter.com
https://www.instagram.com/roland_0fficial

思ったよりツイートしていなかったので、ローランド名言bot、名言集のようなものを探したり、著書から文章を集めて、4106件のローランド様の語録データを作成した。

ローランド様以外のデータは、偶然ダジャレを判定する - Stimulatorで収集した1916747件のツイートデータが手元にあったので、そちらからサンプリングして不正解のデータとした。

 

- 技術的な構成 -

使ったものを端的に挙げていく。


形態素解析機械学習モデルに関しては、以下の記事に書いたものを利用した。

vaaaaaanquish.hatenablog.com

以下repo内にrust製の形態素解析器であるlinderaをwasm-packでwasmに変換し、webpack、Next.jsを経由して動かすExampleを公開しているのですぐ再現できるはず。

github.com


自然言語のベクトル化は、上の記事内でもサーベイした通りPyTorchなどを使う幾つかの方法があるが、再現実装が伴ったものが少なかったので、大人しくFastTextを利用した。判定のコアとなる機械学習モデルの部分はXGBoostやLightGBMなど概ねのモデルがC++で書かれているので、Emscriptenでwasmに変換して利用した。これらの詳細は後述する。

フロントはNext.js、ホスティングにGAE、モデルファイルの配信にGCSを利用した。以下のような構成になった。

f:id:vaaaaaanquish:20201226035644p:plain:w500
構成図

最初S3だけで機械学習Webサービスホスティングと銘打って楽しんだりFierbaseを使って遊んでいたが、不便さと通信量に対する料金が怖くなってこの構成になった。

- モデル周りの話 -

FastTextが0.9.2からWebAssenblyのbindingを配信している。

fasttext.cc

元よりFASTTEXT.ZIPの論文*1などモデルの圧縮を頑張ってたのは知っていたのもあって、ちょうど良く試そうとなった。

FastText含むモデルのC++コードをwasmにするにあたっては、Emscriptenを使う。

github.com

普通にgcc、g++やclangのバージョン、リンクなどハマり所があるので、素直に公式のDockerを使うと良い(ハマった)。

$ docker pull emscripten/emsdk
$ docker run -it emscripten/emsdk bash
$ em++ --version
emcc (Emscripten gcc/clang-like replacement) 2.0.11 (6e28e4fa4fa1bc50d58b9ddbbb9603a3cf21ea9e)
Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

基本的にはC++Makefileをそのまま追記してgccをemmに変更し、利用したい関数をEXPORTED_FUNCTIONSに入れてbuildすれば動く。最も参考になるFastTextのEmscriptenのbuild用のMakefileがかなり参考になる。一方masterのMakefileじゃ動かない*2ので、Emscriptenのバージョンを下げてbuildする必要もある。

 
実際にwasmができても単純にwasm-loaderとwebpackを介するとロードできなかった。

github.com

これは、Emscriptenがwasmとのグルーコードとして吐き出すjsがかなりヤンチャな事と関係している。hoge_wasm.js内のimport.meta.urlをdirnameに書き換えた*3上でhoge_wasm.wasmへのpathを書き換え、hoge_wasm.wasmをNext.jsの静的配信に切り替えた。その上でグルーコードをまるっと書き換えてDynamic Importで読み込めるように書き換えた(別途公開します)。

LightGBMのバイナリもPython bindingで学習したものをwasmから読み込もうとした際に、全ての結果が0になる謎の現象に当たったので、一旦C++でtrainスクリプトを動かしてloadすると上手くいった。学習中にLGBMのコード読んだがこれはイマイチ原因がわからなかった(LGBMのissueに唯一あるwasm関連のissueも「wasm対応出来てるか知らんけどcloseで」となっているので誰も分からない説がある*4)。

 
最初、全てのモデルファイルを合計して雑なフロントから読み出してみると、1266MBをロードしていた。犯罪級だ。

FastTextに量子化の機能が付いているのは事前にF&Qで見ていたので、それを試した。
FAQ · fastText

How can I reduce the size of my fastText models?
fastText uses a hashtable for either word or character ngrams. The size of the hashtable directly impacts the size of a model. To reduce the size of the model, it is possible to reduce the size of this table with the option '-hash'. For example a good value is 20000. Another option that greatly impacts the size of a model is the size of the vectors (-dim). This dimension can be reduced to save space but this can significantly impact performance. If that still produce a model that is too big, one can further reduce the size of a trained model with the quantization option.

元の論文と照らし合わせながら、パラメータを変更して何度かtestしてみて、精度が落ちなさそうな所で以下に落ち着いた。

./fasttext supervised -input ../train_ft.csv -output ft_min -bucket 40000 -epoch 25 -wordNgrams 2 -cutoff 10000 -retrain -qnorm -qout

gbdtもpruningやcompressingみたいな単語でググれば何か先行事例あるだろと思って探して、ちらほらあるものの、目ぼしい物を見つけられなかった。「Lossless (and Lossy) Compression of Random Forests*5」みたく、最小の表現モデルを探したり寄与の低い枝を切ったり面白い分野だと思うが、まあでも今はDNNとかの方が圧縮できるしあまりという感じなのだろうか…

これらは一旦愚直に「木の深さ」「木の数」を制限していき、モデルサイズをやりくりする方法を取った*6

最終的に全体で241MB までダイエットした。あと1桁落としたかったが、精度が犠牲になるならまあ同意を取ろうという事で今回は妥協した。

同意を取る画面で私がローランド様のツイート全部見た上で厳選したツイートが見れるのでそれで我慢してもらう。

 

- おわりに -

12月頭にWebAssenbly nightというイベントがあって面白かったので、なんかコツコツ勉強していたら出来ていた。ありがとうWebAssenbly night。

emsn.connpass.com

Webサービスのアイデアは20秒くらい、フロントエンドも最近慣れてきて1日かからずできたが、wasm buildして配信するまで結局1週間くらい唸った。まともな情報もないし、育児くらいしんどかった。

信頼できるのは以下だけで、後の情報は絶対古いか動かないかが入ってくるので無視した方が良いと思う・

とりあえずこのチュートリアルをなぞるだけにして頼むから。
この記事の内容も最悪誤情報になるなと思った所は削った。

 
次はPyTorch WebAssenblyを試すのと、最近やっているフロントエンド周りの勉強のまとめを書ければと思う。


 f:id:vaaaaaanquish:20201226051027p:plain:w0

*1:JOULIN, Armand, et al. Fasttext. zip: Compressing text classification models. arXiv preprint arXiv:1612.03651, 2016. https://arxiv.org/abs/1612.03651

*2:https://github.com/facebookresearch/fastText/issues/1166

*3:https://stackoverflow.com/questions/60936495/module-parse-failed-unexpected-token-937-with-babel-loader

*4:https://github.com/microsoft/LightGBM/issues/641

*5:PAINSKY, Amichai; ROSSET, Saharon. Lossless (and lossy) compression of random forests. arXiv preprint arXiv:1810.11197, 2018. https://arxiv.org/abs/1810.11197

*6:How to Tune the Number and Size of Decision Trees with XGBoost in Python https://machinelearningmastery.com/tune-number-size-decision-trees-xgboost-python/

Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類

- はじめに -

RustでNLP機械学習どこまでできるのか試した時のメモ。

Pythonどこまで脱却できるのか見るのも兼ねて。

コードは以下に全部置いてある。
GitHub - vaaaaanquish/rust-text-analysis: rust-text-analysis

- 形態素解析 -

Rustの形態素解析実装を調べると、lindera-morphology/lindera を使うのが有力候補となりそうである。sorami/sudachi.rsagatan/yoinnakagami/awabi のような実装もあるがメンテは止まっている様子である。

linderaメンテナのブログ。
Rust初心者がRust製の日本語形態素解析器の開発を引き継いでみた - Qiita

neologd

linderaはipadic-neologd含む辞書作成ツール等もRustプロジェクト内で作成されている。まず、ipadic-neologd辞書を取得し、linderaで扱えるようにする。

以下ツールを使う。
github.com

READMEの通りに進める。

# neologdの辞書をlindera用に
$ cargo install lindera-ipadic-neologd-builder
$ curl -L https://github.com/neologd/mecab-ipadic-neologd/archive/master.zip > ./mecab-ipadic-neologd-master.zip
$ unzip -o mecab-ipadic-neologd-master.zip
$ ./mecab-ipadic-neologd-master/bin/install-mecab-ipadic-neologd --create_user_dic -p $(pwd)/mecab-ipadic-neologd-master/tmp -y
$ IPADIC_VERSION=$(find ./mecab-ipadic-neologd-master/build/mecab-ipadic-*-neologd-* -type d | awk -F "-" '{print $6"-"$7}')
$ NEOLOGD_VERSION=$(find ./mecab-ipadic-neologd-master/build/mecab-ipadic-*-neologd-* -type d | awk -F "-" '{print $NF}')
$ lindera-ipadic-neologd ./mecab-ipadic-neologd-master/build/mecab-ipadic-${IPADIC_VERSION}-neologd-${NEOLOGD_VERSION} lindera-ipadic-${IPADIC_VERSION}-neologd-${NEOLOGD_VERSION}

# lindera-cli で検証
$ cargo install lindera-cli
$ echo "すもももももももものうち" | lindera
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
$ echo "すもももももももものうち" | lindera -d ./lindera-ipadic-2.7.0-20070801-neologd-20200910
すもももももももものうち	名詞,固有名詞,一般,*,*,*,すもももももももものうち,スモモモモモモモモノウチ,スモモモモモモモモノウチ
EOS

neologdが利用可能な状態になっている。ここで作成した./lindera-ipadic-2.7.0-20070801-neologd-20200910なる辞書データは、後でRustスクリプト上で使う。

lindera

CLIではなくRustからneologdを呼ぶ。本体は以下のrepoになる。
github.com

linderaのREADMEに先程作成したneologdの辞書を流す例。

use lindera::tokenizer::Tokenizer;
use lindera_core::core::viterbi::Mode;

fn main() -> std::io::Result<()> {
    let mut tokenizer = Tokenizer::new(Mode::Normal, "./lindera-ipadic-2.7.0-20070801-neologd-20200910");
    let tokens = tokenizer.tokenize("すもももももももものうち");
    for token in tokens {
        println!("{}", token.text);
    }
    Ok(())
}
$ cargo run
すもももももももものうち

形態素解析はこれで一旦良さそう。トークナイズ結果は、mecabフォーマット等でも出力できるので、自然にPythonから移行したりできそうである。

- Text Processing、Embedding -

NLPでよく使うような特徴量、Count Vector、TF-IDF、fasttext、BERT、…辺りを扱いたい。

PyTorchのRust Bindingが一番活発に開発されていて、候補として最も良さそう。
github.com

tch-rsに依存した、guillaume-be/rust-bertなどもある。


TF-IDFは、 ferristseng/rust-tfidfafshinm/tf-idf など野良の実装が見つかるが、メンテも止まっていてあまり使いやすい実装のクレートは現状見当たらなかった。


今回は、facebookresearch/fastTextの公式実装のRust Bindingを実装している以下を使う。
github.com

fasttextの実装も含むのでbuildにcmake必須。macならbrewで入れておく。

$ brew install cmake
extern crate csv;
use lindera::tokenizer::Tokenizer;
use lindera_core::core::viterbi::Mode;
use fasttext::FastText;
use fasttext::Args;

...

    // 一度csvに書き込む
    let mut tokenizer = Tokenizer::new(Mode::Normal, "./lindera-ipadic-2.7.0-20070801-neologd-20200910");
    let mut file = File::create("./data/train_fasttext.csv")?;
    for (sentence, label) in zip(&train_sentences, &train_labels){
        let row = tokenizer.tokenize(&sentence).iter().map(|x| x.text).collect::<Vec<&str>>().join(" ");
        write!(file, "__label__{}, {}\n", label, row)?;
    }
    file.flush()?;

    // 先程作成した __label__1, bar の形式のcsvを入力としてbinを生成
    let mut fasttext_args = Args::default();
    fasttext_args.set_input("./data/train_fasttext.csv");
    let mut model = FastText::default();
    let _ = model.train(&fasttext_args);
    let _ = model.save_model("./data/fasttext.bin");

公式実装のBindingなので、パラメータは以下を見れば良い。
List of options · fastText

このfasttext model自体supervisedに分類を解いてtrainさせれば、この時点でテキスト分類のpredictはできる。

- XGBoost -

一般的にfasttextだけで問題が解ける場合は少ないので、特徴量を追加してxgboostのようなモデルを挟む事になる。

gbdtに関連したものだと、以下が最も更新されていて良さそう。
github.com

今回は、公式実装のRust Bindingなのでこちらを選ぶ(最終更新が2年前でxgboost 0.8を使っていて作者も音沙汰がない様子ではあるが、PythonC++から触っている馴染みのxgboostのパラメータやAPIを使いたい)。
github.com

先程のfasttextモデルで文字列をEmbeddingして、DMatrixを作成しBoosterに入れる。更新がなくREADMEに書かれたExampleは動かないので、実装を見比べるか、repo内のExamplesを参考にする。

extern crate xgboost;
use xgboost::{DMatrix, Booster};
use xgboost::parameters::{self, tree, learning::Objective};

...

    // python binding同様にDMatrixを作る
    let train_data_size = train_sentences.len();
    let test_data_size = test_sentences.len();
    let mut train_dmat = DMatrix::from_dense(train_ft_vector_flatten, train_data_size).unwrap();
    train_dmat.set_labels(&train_labels).unwrap();
    let mut test_dmat = DMatrix::from_dense(test_ft_vector_flatten, test_data_size).unwrap();
    test_dmat.set_labels(&test_labels).unwrap();

    // parameter群を設定し多クラス分類でtrain
    let uniq_label = train_labels.unique().len() as u32;
    let eval_sets = &[(&train_dmat, "train"), (&test_dmat, "test")];
    let learning_params = parameters::learning::LearningTaskParametersBuilder::default().objective(Objective::MultiSoftmax(uniq_label)).build().unwrap();
    let tree_params = tree::TreeBoosterParametersBuilder::default().eta(0.1).max_depth(6).build().unwrap();
    let booster_params = parameters::BoosterParametersBuilder::default().booster_type(parameters::BoosterType::Tree(tree_params)).learning_params(learning_params).build().unwrap();
    let training_params = parameters::TrainingParametersBuilder::default().dtrain(&train_dmat).booster_params(booster_params).boost_rounds(5).evaluation_sets(Some(eval_sets)).build().unwrap();
    let bst = Booster::train(&training_params).unwrap();

    // predict
    let mut preds = bst.predict(&test_dmat).unwrap();

特徴量化からmodelによるpredictまでができた。

- 実験 -

実際にlivedoorニュースコーパスに対して、ニュース本文からメディアを分類するタスクを実施する。

以下からデータをダウンロードした。
ダウンロード - 株式会社ロンウイット

RUN wget https://www.rondhuit.com/download/ldcc-20140209.tar.gz
RUN tar -zxvf ldcc-20140209.tar.gz

このデータを以下スクリプトで雑にtrain、testデータに分割した。
https://github.com/vaaaaanquish/rust-text-analysis

Dockerに収めたので、以下をbuildすれば、上記データがダウンロードされtrain、testデータが作成される。
github.com


実際にlinderaで形態素解析し、fasttextでtrain、embedding、XGBoostで分類タスクを実施した。

testに対するclassification_reportは以下のようになった。

              precision    recall  f1-score   support

           0       0.73      0.72      0.72       251
           1       0.73      0.58      0.64       147
           2       0.91      0.94      0.93       251
           3       0.91      0.92      0.92       283
           4       0.80      0.77      0.79       251
           5       0.84      0.80      0.82       232
           6       0.82      0.94      0.88       254
           7       0.78      0.78      0.78       263
           8       0.81      0.83      0.82       278

   micro avg       0.82      0.82      0.82      2210
   macro avg       0.81      0.81      0.81      2210
weighted avg       0.82      0.82      0.82      2210
 samples avg       0.82      0.82      0.82      2210

悪くないので一旦ここまで。

- おわりに -

割と悪くない所までできた。

LightGBMもcoreのBindingを書いてあげれば使えそうだし、もう少しmetricとかpreprocessingのML関連クレート書いていけば良さそう。

fasttexやPyTorchがWebAssenblyに対応しているので、次の記事ではその検証を書く。
WebAssembly module · fastText
WebAssemblyでの機械学習モデルデプロイの動向 · tkat0.github.io

Python脱却にはまだまだ遠い(クレートやメンテナの数が違いすぎるし、EDAなど型なしでやりたい作業も多い)が、期待した結果が得られたので良かった。

 

機械学習パイプライン構築を楽にするgokart-pipelinerを作った

- はじめに -

luigi、gokartで作ったtaskのパイプライン構築をちょっと楽にする(かもしれない)管理するためのツールを作った。

github.com

近年、MLOpsの一部である機械学習のためのパイプラインを構築するためのツールは飽和状態にあるけどそれらと比較してどうなのという話も書く。

gokart-pipelinerを使ってみる

gokartはエムスリー株式会社が開発している機械学習パイプラインOSSである。gokart自体、使った事がないし興味もないという人も居るかもしれないが、一度以下に提示するgokart-pipelinerの例を見てほしい。

# pip install gokart_pipeliner
import luigi
import gokart
from gokart_pipeliner import GokartPipeliner

# taskを定義する
class TaskA(gokart.TaskOnKart):
    def run(self):
        self.dump(['foo'])

class TaskB(gokart.TaskOnKart):
    before_task = gokart.TaskInstanceParameter()
    text = luigi.Parameter()

    def run(self):
        x = self.load('before_task')
        self.dump(x + [self.text])

# パラメータとパイプラインを書く
params = {'TaskB': {'text': 'bar'}}
pipeline = [TaskA, TaskB]

# run
gp = GokartPipeliner()
gp.run(pipeline, params)

luigiやgokartでは、1つのタスクを1classで表現する。TaskAは ["foo"] を保存するだけのタスク。TaskBはparameterに設定したbefore_taskを読み込んで、同じくparameterに設定した text を末尾に追加するタスク。GokartPipelinerがその2つのタスクをよしなに接続し、パラメータと共に実行している。

gokartの良さ

手前味噌にgokartの宣伝をするならば、この時以下のようなメリットがある

  • それぞれのTaskの以下データが別々にハッシュ値付きでpklファイルに保存される
    • self.dumpしたデータ
    • importした全てのmoduleのversion
    • Taskの処理時間
    • Task内で使われた全てのrandom_seed
    • 出力されるログ
    • Taskのクラス変数として設定された全てのparameterの値
  • TaskBのparameterを変えて実行した時は別のハッシュ値で上記ファイルが生成される
  • TaskAに新たにparameterを追加した場合はTaskBのハッシュ値も変わり依存関係を考慮して両方rerunされる
  • TaskA、TaskB間は上記の出力を中間ファイルとしてやり取りされるためメモリに優しい
  • dumpの出力ファイル形式を拡張できる(デフォルトでもcsv、zip、feather、png、…などをサポート)
  • 入出力時のpandas.DataFrameの型、columnチェック機能がある
  • 保存ファイルのディレクトリ構成がPythonスクリプトの構成から自動的に決まる
  • 基本的なnumpy、randomのシードは自動で固定化される
  • SOLID原則をなるべく守りながらコーディングできる
  • 保存された出力データとparameter、hashの管理はthunderboltなる別ライブラリでPython上で行える
  • 並列にタスクが動作してもredis経由でTaskがロックされる

機械学習モデリングにおいて、何をロードして、何を出力し、どのように繋げるか以外の殆どを自動的に決定し保存する仕組みになっている。また、その上でクラス単位でTaskを作る事によるソフトウェア開発における単一責任の原則などを守りやすくなっている。

近年は、機械学習パイプラインツールの戦国時代でもある。他の多種多様なライブラリと比較しても、モデリング時やproductionの再現性のための中間出力が多いし、Pythonコード上で見た時、デコレータや謎のメソッドがチェーンされまくったコードよりは保守性が高くなるはずだ(gokartを学ぶコストさえ払えば)。

例えば「pandas.DataFrameのtext columnから文字列の長さのcolumnを生成する」という処理は、無闇に大きな関数やスクリプトにせず、1つのファイルに1つのTaskとして考えて以下のように書いていく。

class CalcTextLengthTask(gokart.TaskOnKart):
    target = gokart.TaskInstanceParameter()
    __version = luigi.IntParameter(default=1)

    def run(self):
        df = self.load_data_frame('target', required_columns={'id', 'text'}, drop_columns=True)
        df['text_length'] = df['text'].str.len()
        self.dump(df[['id', 'text_length']])

gokartにおいては、1Taskの規模感さえ一致すれば、この書き方以外でコードを書くのは難しく、コードレビューや保守が行いやすい。また、このCalcTextLengthTaskの入力となるtarget taskを変える事で、hash値等のメリットを享受しながら使い回す事ができる。例えば機械学習モデルの汎用的なTrainタスクを書いておいてTaskInstanceParameterのみ変えるといった具合に。

また、parameterで中間ファイルのhash値が変わる事を利用して、__versionのようなパラメータを雑に付けてあげれば、「長さを測る前にstripしてからという処理に変更」した時にversion=2としてcommitしておくことで、あとからgitのlogをblameしたり、出力されるhash値付きのpklファイルを見比べる事でデバッグが行いやすくなる。


加えて、wrapしているluigiとの比較は以下のスライドを参考に。
gokartの運用と課題について - Speaker Deck
何もないluigiを書くよりも書きやすいと思えるはずである。

 

gokart-pipelinerの意義

gokartでモデリングしたり、コンペに出たり、会社での本番運用を重ねていく中でいくつか課題になってきた以下のような点を解決しようとしたのがgokart-pipelinerである。

  • パラメータとパイプラインが密結合しすぎ
  • パイプラインライブラリなのにやればやるほどrequiresメソッドが複雑になる
  • jupyter notebookと行き来するのがダルい
パラメータとパイプラインが密結合しすぎ

パラメータとTaskの動作が分離しているパイプラインは、近年の流行となりつつある。

特にFacebook社の公開したHydraはかなり大きな転機だったように感じる。yamlとデコレータを軸としたパラメータ管理で、yamlファイルさえ管理していればどんなパイプラインを書いても良いし、かなり管理も楽である。

一方でデコレータは闇魔法を生みやすいし、デコレートした謎の巨大な関数を見るのはツライので、もう少しコーディングに制約を持たせた形のパイプラインを作りたいと思っていた。(他人の書いたHydra+mlflowのコード見るのしんどすぎない?)

luigiにもconfigParserを使ったiniやyamlファイルを読んでパラメータとする機能はある。しかし、gokartにもTaskInstanceParameterというやつが居る。これ自体はtaskを依存関係の一部と捉えられる良い機構ではあるものの、Parameterという扱いとしてyamlのように一箇所で管理できないネックがあった。

gokart-pipelinerの場合を見てみる。

from gokart_pipeliner import GokartPipeliner
from ExampleTasks import *

pipeline = [TaskA, {'task_b': TaskB, 'task_c': TaskC}, TaskD]
params = {'TaskA': {'param1':0.1, 'param2': 'sample'}, 'TaskD': {'param1': 'foo'}}

gp = GokartPipeliner()
gp.run(predict, params=params)

pipelineは「Task同士のTaskInstanceParameterによる依存関係のみい」を表し、paramsは「各タスクのそれぞれのluigi.Parameter」を表していて、切り分けられている。元々のluigiのconfig形式にも対応しているので、configファイル、pipeline、paramsをそれぞれ考えつつ、この構成だけ保存しておけば一元管理もできる。

パイプラインライブラリなのにやればやるほどrequiresメソッドが複雑になる

gokartの機能としてrequiresというクラスメソッドがある。これは、読み込むデータを指定するメソッドで、requiresが返す値でluigiが依存タスクを決めている。

先程のタスクをgokart-pipelinerを考えず書いた場合は以下のようになる

class CalcTextLengthTask(gokart.TaskOnKart):
    target = gokart.TaskInstanceParameter()

    def requires(self):
        return self.target

    def run(self):
        df = self.load_data_frame('target', required_columns={'id', 'text'}, drop_columns=True)
        df['text_length'] = df['text'].str.len()
        self.dump(df[['id', 'text_length']])

このrequiresは以下のようにlistやdictを返したりもできる。

    def requires(self):
        return {'target': self.target, 'model': self.clone(MakeModelTask)}

更に複雑に、依存関係やパラメータ、分岐を書く事もできる。

    def requires(self):
        data = TrainTestSplit(data=MakeData(path='/'), split)
        if self.parameter_a == 'var':
            task = MakeModelTask(data=data, param_a=0.1, param_b='foo')
        else:
            task= MakeModelTask(data=self.data)
        return {
            'data': data,
            'model': self.clone(task)}

この状態では、コンペのような実験とコーディングを繰り返す時に、依存関係がどうなってるか把握するのがどんどんしんどくなる。gokartに依存関係Treeを出力する機能があるが、流石にしんどい。こういった複雑なrequiresを集約するエンドポイントになるTaskを作ったりするが、次はそのエンドポイントからしか全体が実行できなくなったりしていくし、エンドポイントが増えれば増えるほど、どのファイルを見て回ればいいか分からなくなる。その上で途中にTaskを挿入したいとなったら、と考えるとただただ辛くなる。

なので、そもそもrequiresを書かない制約を付ければ良い。


例えば、gokart-pipelineでの先程の例を考える。

pipeline = [TaskA, {'task_b': TaskB, 'task_c': TaskC}, TaskD]
params = {'TaskA': {'param1':0.1, 'param2': 'sample'}, 'TaskD': {'param1': 'foo'}}

ここでTaskDは、リストの1つ前のdictを引数にするように、以下のように書いたクラスである。

class TaskD(gokart.TaskOnKart):
    task_b = gokart.TaskInstanceParameter()
    task_c = gokart.TaskInstanceParameter()
    param1 = luigi.Parameter()

    def run(self):
        b = self.load('task_b')    # list
        c = self.load('task_c')    # list
        data = b + c + [self.param1]
        self.dump(data)

requiresメソッドは、TaskInstanceParameterのパラメータ名から、gokart-pipelineが生成する。この制約によって複雑なrequiresを書かれる事もなく、pipelineのlistだけを変数やdictを使って上手く書いてやれば良いだけになる。

jupyter notebookと行き来するのがダルい

gokart自体をjupyter notebookで動かす方法はあるものの、かなりハックじみた方法となる*1

gokart-pipelinerはjupyter notebook上で動く。
gokart-pipeliner/Example.ipynb at main · vaaaaanquish/gokart-pipeliner · GitHub

Task同士は中間ファイルでやり取りされるのでメモリをバカ食いしないし、classである事さえ意識して一般的なソフトウェア開発の心得に沿って書けば、ここで書いたコードをそのままproductionコードにするのも容易になる。

もちろんタスクを動かした後、thunderboltを使って出力ファイルをメモリに読み込むなどして、jupyter上で触っても良い。
github.com

future work

今後上手く使えてきたらできそうなこと

  • pipelineのリストの書き方のベストプラクティスを探る
  • runの返り値として出力データを返したりできるようにする
  • pipelineを決めたら並列化できる所を自動で並列に動作させたりする
  • jupyter notebookからも別のプロセスとして動かす(Taskが動く最中もnotebookが実行できる)

おわりに

試しに作ってみた段階なので、これからコンペに出たり本番運用に使ってみたりして調整していきたい。

gokartは良いぞという話を散々書いたが、gokartは中間ファイルを経由するだけにDataFrameを良く使うテーブルデータでは使いやすく、画像コンペのような所では全然良さを発揮できなかったりする。いやいや画像音声テーブルなんでも自分のツールは共通化しておきたいわ、という人に不向きという所をなんとか改善できればと思ってはいる。

なんとなく形になったら、gokartにマージしてもらって、gokartの定番となっても良い気もする。

がんばろう。

 

*1:機械学習プロジェクト向けPipelineライブラリgokartを用いた開発と運用 - エムスリーテックブログ https://www.m3tech.blog/entry/2019/09/30/120229 を参照すると良い

ダジャレを判定する

- はじめに -

近年、IT業界のダジャレは熾烈の一途を辿っている(ITだけに) 。

類義語を巧みに取り入れたダジャレ、難読化されたダジャレなどが増加し、一体どれで「初笑い」すれば良いのか悩む若者も少なくない。


そのような背景があり、ダジャレを判定するアルゴリズムの開発も盛んである。
ルールベースによる判定では、@kurehajimeが提案、開発したdajarep *1 や、@fujit33によるShareka *2が存在する。特にSharekaは、ルールベースのロジックにも関わらず、反復型とされる種類のダジャレに対して高い精度での判定を可能にしている。また、機械学習モデルを用いた判定手法として、谷津(@tuu_yaa)らが開発したDajaRecognizer *3がある。DajaRecognizerは、多くのルールベースによって子音音韻類似度をPMIとして定義、Bag-of-Words、SVMを利用し、高い精度での駄洒落の機械学習モデルによる抽出を可能としている。


先行事例においては、一部の手法については触れられているものの、具体的な実装、ダジャレの解析まで踏み込んで、ダジャレの判定を行った事例は少ない。本記事は、ダジャレの判定、検出のタスクを例に、Pythonを利用した自然言語処理機械学習による実装例を紹介し、ダジャレ研究ないし「面白いとは何か」について計算機科学の側面から言及するものである(研究だけに)。

 

- ダジャレについて -

本記事で検出対象とするダジャレとは、ユーモアを含む1文ないし2文の短な文章の事である。

ダジャレに内包される「ユーモア」については、既に表層的な考察がいくつか行われ、様々な分類方法が提案されている。
谷津、荒木らの『駄洒落の面白さにおける要因の分析』*4 では、fig.1のように「音韻・統語的要因」「語彙的要因」「複合要因」の3つのセクションに分類している。@fujit33によるSharekaにおいては、fig.2のように「反復型」「潜在表現重複型」の2つの大きなセクションに分けた上で、反復型を更に「完全反復型」「不完全反復型」「変形反復型」の3つに分類している。

f:id:vaaaaaanquish:20201130222645p:plainf:id:vaaaaaanquish:20201130222928p:plain
左:fig1. 谷津、荒木らによる分類 右:fig2. @fujit33による分類

中でも音に関する分類、考察は非常に多く、@_329_は「ダジャレは音が非常に重要である」としながら、「潜在的な意味からくるダジャレもある」「人間の知識の成り立ちにも通づる」*5としている。その他、オノマトペに着目した報告 *6 や、対話や自己完結といった形で分類する考察 *7 もある。


実際に筆者も所属企業内向けのLTにて、「マルコフ連鎖によるダジャレ生成」「BERTによるダジャレ生成」「Elasticsearchを用いたダジャレ対話システムの構築」について話している。

文字列(ダジャレを言いシャレ) - Speaker Deck

上記スライドP11、P13にはそれぞれの生成モデルの結果サンプルを掲載しているが、これらの結果から「音に関する要素を含まないダジャレはダジャレと判断しづらい」事や「逆に意味空間からだけではダジャレの生成は難しい」事が見て取れる。また、後半の検索によるダジャレ対話システム実運用では、音や単語でのfuzzy searchだけでなく、Elasticsearchにおけるvector fieldsに埋め込み表現を入れる事で、対話相手に「上手い」と思わせるようなダジャレの検索、評価を行えた例を示している。

以上から、ダジャレにおけるユーモアとは、以下のような要素で構成されると筆者は考えている。

  • 音の繰り返し
  • 類似語、同意義語などの意味の側面
  • 関連単語を利用した勢い(文脈崩壊ダジャレなど)
  • 対話における前後の文脈

こういった先行研究については、以下のRepositoryのREADMEにまとめてある。
github.com
また、上記Repositoryには、複数のダジャレ掲載サイトをクロールするスクリプト群が含まれている。
学術分野では、荒木氏が公開している6万件ダジャレを含むダジャレデータセットが一般的に利用される*8。本記事では、そちらのデータセットを利用せず、上記スクリプトで取得したデータと、独自に社内外で収集したデータを合わせた121789件のダジャレデータを取り扱い、実験を進める。

クローラは、sleepの設定により全体のデータを収集するまでに2週間程かかるため、再現を行う際は注意が必要となる。学術目的であれば、ダジャレデータセットを申請すると良い*9

 

- ルールベースによる判定 -

本記事では、ルールベースのベースラインとして、@fujit33によるSharekaを参考に進める。
qiita.com
上記記事では、Sharekaクラスの一部メソッドが記述されておらず、記事の更新も見られないため、sentence_max_dup_rate、list_max_dupという2つのメソッド名から推察される動作を自前で書いたものを以下に設置した。
dajare-detector/shareka_v1.py at main · vaaaaanquish/dajare-detector · GitHub


Sharekaの挙動は、文章をカタカナに変換し正規化した後、n文字分のwindowをズラしながらカタカナの繰り返しを探索するものである。

f:id:vaaaaaanquish:20201211122321p:plain:w500
fig3. 先行事例Sharekaのロジック(n=3の例)

 

先行事例の再現と考察

実際に上述したスクリプト用いてSharekaアルゴリズムを、window sizeとなるnを変化させながら121789件のダジャレデータに対してのみ適応した結果を、以下に示す。

window size true false 検出率
n = 2 111924 9865 0.9190
n = 3 88199 33590 0.7242
n = 4 61807 59982 0.5075
n = 5 40348 81441 0.3313

また、上記の結果から、以下のような考察が立った。

  • 元記事にある通りnが小さい時の方がダジャレを検出できる
    • n=2はダジャレでない分を混ぜた場合に「ダジャレと判定されたが実はダジャレじゃなかった(False Positive)」が増大する事が知られている
    • nが小さい時ダジャレ文のダジャレではない所にも反応している可能性があるのではないか
    • nが大きい場合は検出数こそ少ないもののより確実にダジャレと言えるのではないか
    • validationデータを作成する時はこのnも考慮した方が良いのではないか
  • n=2でも抽出できない1万件弱はどういったデータか
    • それらに対する改善策はあるか
  • カタカナの正規化、繰り返しで9割のダジャレ候補が抽出できる
    • Webのようなダジャレ掲載サイトには「音」のような分かりやすい例が大半を占めているのではないか
    • 実際のダジャレ空間は広く「音」で検出できないデータこそ拾えると面白いのではないか
    • ダジャレでないデータセットを追加する時は音、意味それぞれの属性、量を考える必要がある

繰り返し判定の精度の確認

1つ目の考察「window sizeが小さい時の方がダジャレを検出できるが、小さすぎると誤検出が増える」については、実際にデータを見る事で確認できた。

キャラメル好きな人から、空メール!

このダジャレは「キャラメル」と「空メール」の語感が似ているというダジャレであるが、n=2の場合「好きな人から」と「空メール」のカラの部分が繰り返しとみなされ、ダジャレであると判定されてしまう。こういったダジャレが、実空間での判定時に「ダジャレと判定されたが実はダジャレじゃなかった(False Positive)」が増大する原因になっていると言える。
元記事は「できる限りダジャレを救う」ことを目的としているため良さそうであるが、実際にモデルに落とし込む際には考慮が必要となる。

また、window sizeが大きい場合の検出ダジャレ例をいくつか以下に示す。

州駅、工場で収益向上
家なかったなんて、言えなかった!
生憎、揉んだ愛に苦悶だ
SCANDALが「スキャン、だるっ!」
そう来た!異色の早期退職!
...

どれも短いながら工夫が凝らされたダジャレになっており、100件ランダムにサンプリングして目視した結果では、全て上記のような「どう取ってもダジャレと言える」ものであった。このことから、window sizeが大きい程、確実にダジャレを取得できる事がわかる。

 

検出ルールの改良

2つ目の考察である「n=2でも抽出できない1万件弱はどういったデータか、それらに対応可能か」について、実際にSharekaアルゴリズムが検出できなかった例を目視した所、いくつかのパターンによって検出できない例がある事がわかった。それらの中で、改善可能であるいくつかの点については改良を行った。

 

英単語の読み

 
第一に、以下のような英単語入りのダジャレが目立った。

Missは見捨てよう!
斧が折れちゃった…「Oh!No!!」
Hey!妖怪も併用かい?

ルールベースロジックでは、内部で文章を読みに変換する部分に利用しているmecabの辞書に以下のように英単語に対して日本語の読みが入っている場合があり、その場合は対応できていた。

Docker 名詞,固有名詞,一般,*,*,*,Docker,ドッカー,ドッカー
yesterday 名詞,固有名詞,組織,*,*,*,*

この辞書データを利用することで、現状のロジックでも、IT業界で使い古された著名なダジャレである「Dockerでどっかーん」は検出可能である。一方、heyやyesterdayのような一般的な単語を用いたダジャレは対応できていない。そのため別途、英単語をカタカナに変換する必要がある。幾つかの方法があるが、今回は簡易にalkanaモジュールを試した。

import alkana

print(alkana.get_kana("Hey".lower()))  # ヘイ
print(alkana.get_kana("Amazing".lower()))  # アメイジング
print(alkana.get_kana("yesterday".lower()))  # イエスタデイ

alkanaモジュールの内部では、事前に用意された辞書に対して単語を当てているだけではあるものの、こちらを利用することで6万単語以上をカバーすることができた。alkanaの中に単語はあるかな状態である。
こちらを組み込んだモデルをSharekaV2とし、以下に設置した。
dajare-detector/shareka_v2.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV2によって、新たに202個のダジャレの検出が可能となった。

 

形態素解析の複数パターン生成

第二に、mecabによるカタカナ変換時に形態素解析にい失敗しているパターンが見られた。

お骨を置こつ → オホネヲオコツ
さかなクン魚食う → サカナクンギョクウ
オタ会ってお高い? → オタアテオコウイ?

そもそもダジャレは日本語の文章としては崩壊している場合が多く、無茶な読ませ方をする事でダジャレとして成立させている場合も多い。こちらも辞書拡張など幾つか方法論が考えられるが、mecab形態素解析のパターンをparseNBestInitを利用して複数出して探索する事で解消した。

import MeCab

text = 'オタ会ってお高い?'
print(text)
mecab = MeCab.Tagger("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
mecab.parseNBestInit(text)
for i in range(11):
    node = mecab.nextNode()
    while node:
        n = node.feature.split(",")
        if len(n)==9:
            print(n[7].replace('*', ''), end=' ')
        node = node.next
    print()

オタ会ってお高い?
オタ アッ テ オ コウイ
オタ カイ ッテ オ コウイ
アッ テ オ コウイ
カイ ッテ オ コウイ
アッ テ オ コウイ
アッ テ オ コウイ
カイ ッテ オ コウイ
カイ ッテ オ コウイ
オタ カイ ッテ オ コウイ
オタ アッ テ オ タカイ
オタ カイ ッテ オ タカイ

この方法は、読みのパターンを出せば出すほど、先程示したような誤検出を増やす可能性があるが、ルールベースでは一度許容するものとした。こちらを組み込んだモデルをSharekaV3とし、以下に設置した。4,5件の誤検出はご検討願おう。
dajare-detector/shareka_v3.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV3によって、読みパターンを出来る限り出しどれかがダジャレと判定されるか、というロジックで新たに1979個のダジャレの検出が可能となった。

 

ローマ字読みによる音の繰り返し判定

第三に、以下のような長音を含むダジャレや、文中の一音を変更しリズムをとるダジャレに注目した。

モーターを壊してもうた
船を呼ぶね
孫がマンゴーを食べる

ここで「モーターを壊してもうた」は、Sharekaロジックでは「モタヲコワシテモウタ」に変換され、繰り返しが発生しないのでダジャレとして検出できない。

そこで、先程のカタカナ変換ロジック適応後、pykakasiモジュールを用いてローマ字表記に変換した。

from pykakasi import kakasi

k = 'モーター ヲ コワシ テ モ ウタ'
print(k)
for r in ['Hepburn', 'Kunrei', 'Passport']:
    kks = kakasi()
    kks.setMode('K', 'a')
    kks.setMode('r', r)
    conv = kks.getConverter()
    print(conv.do(k))

当たる範囲を広げるためにHepburn式以外も採用した。

モーター ヲ コワシ テ モ ウタ
mootaa wo kowashi te mo uta
mootaa o kowasi te mo uta
mootaa wo kowashi te mo uta

このローマ字列に対し、前述のようなwindow sizeのロジックをローマ字に適応すると、過剰にマッチが発生してしまうため「全ての単語に対してLevenshtein距離が1以内の文字列が文中にあるかどうか」を判断基準とした。

f:id:vaaaaaanquish:20201211122132p:plain:w500
fig4. ローマ字パターン判定
from fuzzysearch import find_near_matches

def roman_match(sentence, min_length=3):
    words = [x for x in sentence.split(' ') if len(x)>=min_length]
    choices = ''.join(sentence.split(' '))
    for word in words:
        if len([x for x in find_near_matches(word, choices, max_l_dist=1) if len(x.matched)>=len(word) and x.matched!=word])>1:
            return True
    return False

roman_match( 'keeki de keiki zuke' )  # True

このロジックによって ouoo のような長音、カタカナによって発生する違いを吸収しつつ、「音を変化させるダジャレ」についても対応した。

おっと、こちらを組み込んだモデルもSharekaV4として以下に設置した(音だけに)。
dajare-detector/shareka_v4.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV4によって、新たに4848個のダジャレの検出が可能となった。音を変化させるダジャレがそれなりの数あることが分かった。

ルールベースの改良結果

ここで、V4までの差分を以下に示す。V4では、V3までのカタカナの反復を想定したアルゴリズムとは別で単体で検出数を測る事ができるため、そちらも別途測定を行った。

前バージョンとの差分 取得数割合 V3,V4のみの取得数
Shareka (origin, カタカナ反復) 111924 0.9190 -
SharekaV2 (英単語読み) 202 0.9207 -
SharekaV3 (読み複数生成) 1979 0.9369 114105
SharekaV4 (ローマ字fuzzysearch) 4848 0.9767 108508

実際にダジャレデータの中の97.6%を判定する事ができた。また、Sharekaのようにwindowをズラして反復を検出した場合とローマ字に直した場合では、ローマ字に直す時点で情報が落ちているダジャレも多いため、ダジャレの検出という意味で前者が良い事がV3、V4の比較でわかった。このルールベースの手法は再現率(recall)を極端に高める方法であるため、実世界での応用については、後述するようなダジャレでないデータと混ぜた上での検証が必要となる。

そもそものデータに関する考察

3つ目の考察である「反復と音を利用したルールベースロジックで9割以上ものダジャレが検出できる事は本当に正しいのか」という点について、実際に目視で検出できなかった例を確認した。SharekaV4でダジャレデータセット内で検出できなかったダジャレは2836件であった。検出できなかった例を目視で見ていくと、幾つかのパターンが見受けられた。

# 日英翻訳という知識ベースの例
3月のテーマソングは、マーチ!
日曜は起こさんでー!
猫がキャッと鳴く
驚異のバスト
手紙が破れたー

# 音が遠い、判定できない例
メガネのメーカーね
斜めに7メートル進む
令和の0話

# カッコや+、&等の記号の読みを含む例
(E)←この文字、かっこいい
バター&アンドーナッツ
3+5が解けるサンタすご

# 文脈が崩壊しているが定型句と勢いをユーモアとする例
時すでにお寿司!
今、何時?ゼロ歳児!
飢えすぎ謙信

# 「滋賀が近江国であった」のような事前知識を要求する例
これ滋賀のおーみやげ!
葡萄が驚いた「キョホー!」
このガンダム、起動せんし!

日英翻訳については、名詞に絞って翻訳する事で対応が可能そうである。遠い音についても、数字、記号、アルファベットの対応表を作る事で更に精度改善が見込めそうである。一方、勢いを重視するダジャレ、事前知識を要求するダジャレについては、ルールベースでの対応の難易度は高い。古典的にはWordNetのような単語の類義関係を用いて語を増強するといった解決策がありそうではあるし、最近だと単語埋め込みにするのが一般的であるが、いずれにしても「起動せんし → 機動戦士」「キョホー → 巨峰」のような揺れの吸収を行う複雑なタスクとなる事が考えられる。

また、ダジャレデータセット自体の課題として以下のようなノイジーなデータがある事もわかった。

# ユーザ投稿型のサービスでユーザ同士のやり取りがダジャレとして投稿される場合
~~は面白くないから, (・∀・)イイネ!!
 
# 汎用的、暴力的などのワードを伏せ字で置き換えた場合
雑煮は○臓にいい

自前で作成したダジャレデータセットは、ある程度事前にノーマライズ処理をかけているが、こういったデータが含まれる事があり、Sharekaのようなルールベースロジックでご検出されている場合や、形態素解析が難しくなる事も考慮する必要がある事がわかった。先に述べた荒木らが公開している6万件ダジャレを含むダジャレデータセットでは、目視やクラウドソーシングによるチェックが行われているらしく、そちらによる評価もfuture workとなると考えられる。


本記事では、ダジャレデータセット内の非検出ダジャレの数が少ないことから、一度ルールベースでの精度向上については、ここまでとする。

ダジャレ、非ダジャレデータセットの作成

まず試しに青空文庫の作品名を対象とし、前述のルールベースロジックを適応してデータセットを構築した。青空文庫のデータのみでは課題がある事がわかったため、別途Twitterを利用したデータセット作成にも取り組んだ。

また、ここで前述のルールベースのクラスが若干使いにくく速度面でも課題が出てきていたため、一度リファクタを実施した。スクリプトを以下に示す。
dajare-detector/dajare_detector.py at main · vaaaaanquish/dajare-detector · GitHub


青空文庫を利用したデータセット作成

ルールベースロジックには「カナの繰り返しを判定するワード長(window_size)」「fuzzysearch元となるローマ字節の最小単位(min_roman)」「mecabで候補パターンを生成する最大数(mecab_pattern_num)」の3つのパラメータがある。ここでは、上記スクリプトを利用し、一度mecab_pattern_numを出来る限り大きく50で固定した上で青空文庫タイトル17493件、ダジャレデータセットを各パラメータで判定した結果が以下のようになった。

parrameter 青空文庫ダジャレからの検出数 ダジャレデータセットからのダジャレ検出数
window_size=2, min_roman=2 2853 (0.16) 118953 (0.98)
window_size=3, min_roman=2 1389 (0.079) 95932 (0.79)
window_size=4, min_roman=2 1138 (0.065) 80485 (0.66)
window_size=2, min_roman=3 2310 (0.13) 95382 (0.78)
window_size=3, min_roman=3 601 (0.034) 78014 (0.64)
window_size=4, min_roman=3 297 (0.017) 57647 (0.47)
window_size=2, min_roman=4 2185 (0.12) 95316 (0.78)
window_size=3, min_roman=4 439 (0.025) 77495 (0.64)
window_size=4, min_roman=4 127 (0.0073) 56483 (0.46)

元ロジックとなっている先行事例のSharekaでの検証同様、繰り返し判定の幅の単位が小さい程、ダジャレ検出数が増えるが誤検出も増える結果となった。また、前述の通り、ダジャレデータベースからも抽出できていなダジャレが存在しており、このダジャレを判定しながら誤検出を下げる試みが必要である事も示された。

実際に青空文庫から間違った例2853件を目視で確認したが、ダジャレと思えるタイトルは見当たらなかった。実際に誤検出された例を以下に示す。

百万人のそして唯一人の文学 - 1973 - 青野季吉

このタイトルは以下のように分解され、「ニンノ」が繰り返されている事からルールベースではダジャレと判定される。

ヒヤクマンニンノソシテタダイチニンノブンガク
['ヒヤク', 'ヤクマ', 'クマン', 'マンニ', 'ンニン', 'ニンノ', 'ンノソ', 'ノソシ', 'ソシテ', 'シテタ', 'テタダ', 'タダイ', 'ダイチ', 'イチニ', 'チニン', 'ニンノ', 'ンノブ', 'ノブン', 'ブンガ', 'ンガク']

この例から分かるように、音の繰り返しは日常的に発生し得る事がわかる。

 
上記の結果を参考に、ここでは、青空文庫およびダジャレデータセットのデータを活用し、以下の2つのデータセットを作成した。

青空文庫(負例) ダジャレデータセット(正例)
データセットA 全データ 17493 ランダムサンプリング 17493
データセットB ルールベースで誤検出したもの 2853
誤検出されなかったデータからサンプリング 2853
ルールベースで検出できなかったもの 2836
検出できたものからサンプリング 2836

このデータセットでは、人類は半分以上の会話でダジャレを言いたいものであると仮定し、実世界内でのダジャレの分布などは加味しない。

 

Twitterを利用したデータ作成

実世界上での応用に向け、TwitterAPIを利用し、別のデータセットも作成した。上記のデータセットは、実世界でのダジャレの割合だけでなく、文章内に出てくる単語の分布も大きく変わってしまう課題が存在する。そのため、ダジャレデータセットから形態素解析によりダジャレに出てくる名詞 51551 件を抽出、Twitterから1単語に対して100件のツイートを取得する検索スクリプトを以下のように作成した。

# RTやリンク付きツイートにダジャレが少ないと仮定
import tweepy
api = tweepy.API(...)
q = f'{word} -RT lang:ja -filter:links -filter:retweets'
api.search(q=q, tweet_mode='extended', result_type='recent', count=100)

dajare-detector/twitter_crawler.py at main · vaaaaanquish/dajare-detector · GitHub

このスクリプトを一週間程動作させ、2020/11/10日時点で最新の、ダジャレデータに使われる単語を含む、50字以内の 1916748件 のユニークなツイートデータを取得した*10。このデータにおいては、真の正解(このデータ内にダジャレが本当はいくつ含まれるか)の判断が難しいため、データセットA, Bでモデルを作成した後、実世界でのサンプルとして利用し、検出できたもののみ黙々と目視で黙視する。

ツイートデータ
データセットC 全データ 1916748
データセットD ルールベースで検出されたもの 20000
検出されなかったものからサンプリング20000

データセットCに対してルールベースの判定ロジック (window_size=2, min_roman=2) を適応したところ、1574616件 (0.821) が抽出された。実世界に先述のルールベースロジックを適応した時、その結果の多くが誤検出となる事がわかる*11。そのためデータセットDは全体の1%である2万件をルールベースの判定を基準としてランダムサンプリングし作成した。データセットC、Dは殆どがダジャレでないが一部ダジャレの可能性があるノイジーなデータとして扱う。

機械学習を用いたダジャレの判定

先行事例であるDajaRecognizerでは、子音に関する素性およびBag-of-Words、SVMを用いた識別でAUC 90%を達成したとの報告されている。下記のRepository内にあたる。
gitlab.com
ロジックに関する解説のdocumentにも解説がある通り、ロジックとしては以下のようになっている。

  • ルールベース
    • 文章をmecab形態素解析 (mecabによる変形、連続をどちらも取得)
    • それぞれモーラ音素に変換 (1字ごとのローマ字に)
    • Needleman-Wunsch algorithmで子音と母音それぞれでアライメント
    • アライメントした音のペア対応から共起頻度を取る
  • BoW

ルールベースを本記事内で利用しているロジックと比較すると、NWで単語の音が似たペア行列を作成するまではほぼ同等の作業であるが*12、ペアの共起頻度を取り出現頻度を用いた自己相互情報量のように扱っている点が工夫されている( 前述のような「3 人の (ninno)」は一般的な文章において発生しがちで「 麦の向き(mugi no muki)」のような音の繰り返しは一般的に発生しにくい事からダジャレの可能性が高い、という情報を数値化し閾値で判定するイメージ )。

BoWは単語の出現回数をベースとした手法であり、事前に実施したマルコフ連鎖によるダジャレ生成がそれなりのダジャレを生成していた事からも、出現ベースの分類はある程度上手くいくだろうという事がわかる(「布団」と「吹っ飛ぶ」という単語を同じ文章に入れる人がダジャレ以外を想定している可能性は限りなくゼロに近い事を判定基準にするイメージ)。

DajaRecognizerにはNgram、RNNによる素性生成の記述があるが、コードを見る限り実際は実装されていないようである。

今回は、上記の手法を参考にしつつ、お手製featureおよび、Deep Neural Networkベースの手法によって判定を試みた。

 

Sentence-BERTを用いたベクトル化

一度DNNベースのモデルによる特徴量を生成しEDAを行う。特徴量化にはSentence-BERT*13を用いた。*14

実装と学習済みモデルを簡易に利用する方法として、以下が公開されている。
github.com

上記のsentence bert modelは、日本語の学習済みモデルを@sonoisa氏が公開している*15。そちらのモデルをロードし、文章をfeature vectorに変換するスクリプトを以下に示す。

# pip install git+https://github.com/sonoisa/sentence-transformers
import tempfile
import tarfile
import os
import requests
from sentence_transformers import SentenceTransformer

def load_sentence_bert_ja():
    url = "https://www.floydhub.com/api/v1/resources/JLTtbaaK5dprnxoJtUbBbi?content=true&download=true&rename=sonobe-datasets-sentence-transformers-model-2"
    with tempfile.TemporaryDirectory() as d:
        model_path = os.path.join(d, 'training_bert_japanese.tar')
        with open(model_path, 'wb') as f:
            for chunk in requests.get(url, stream=True).iter_content(chunk_size=1024):
                f.write(chunk)
        tarfile.open(model_path).extractall(d)
        model = SentenceTransformer(model_path.strip('.tar'), show_progress_bar=False)
    return model

model = load_sentence_bert_ja()
sentence_vectors = model.encode(['ウシがあばれてモーたいへん'])
print(type(sentence_vectors[0]), len(sentence_vectors[0]))    # <class 'numpy.ndarray'> 768

上記でデータセットAを特徴量化し、正例負例それぞれ1000件ランダムサンプリング、t-SNE*16によって2次元で可視化した所、以下の結果が得られた。

f:id:vaaaaaanquish:20201205143053p:plain:w400
fig5. 正例負例1000件ずつt-SNEで可視化

綺麗に分割とまではいかないが、学習済みのsentence bertから得られる特徴で分類タスクを実施できそうな事がわかる。BERTでバーっとやれば良いわけだ。

 

頻度ベースの特徴量生成

先行事例であるNeedleman-Wunsch algorithmによるアライメントと同等の処理を行うため、ルールベースで抽出した繰り返しカタカナ、繰り返しローマ字から、相互情報量を算出し、ルールベースで繰り返しが見つかる場合は特徴量に追加、見つからない場合はnp.zerosを追加した。

f:id:vaaaaaanquish:20201211041050p:plain:w500
fig6. 繰り返しをmatrixにしてPMIへ

その他お手軽特徴量

一般的なNLPタスクで利用されるものとして、ダジャレ判定において有用そうな以下の特徴量を利用した。

  • 文字列の長さ
  • mecab分割単語数
  • TF-IDF vector
  • 音の繰り返しの有無(カナ、ローマ字をwindow_size、min_roman 2~4ごとに)

実験

識別器にはLightGBM (LGBM) を利用した。またハイパーパラメータは全てOptunaによる最適化を実施した。
また、データセットよりtest, validationとして20%ずつ分割した。

BERT+LGBM

BERTによる特徴量のみの場合の、データセットAのtestセットに対するclassification_reportを以下に示す。

              precision    recall  f1-score   support
           0       0.92      0.80      0.85      3378
           1       0.97      0.99      0.98     20480
    accuracy                           0.96     23858
   macro avg       0.94      0.89      0.92     23858
weighted avg       0.96      0.96      0.96     23858

t-SNEでの可視化の通り、BERTを利用する事のみで、ある程度の精度でダジャレの認識を行える事が示された。

実際にルールベースで検出できず、BERT+LGBMにより検出されたダジャレを以下に示す。

この石はなんでストーン
雪が積もってまスノー!
今、何時?法隆寺!

「今何時?〇〇zi」「君何部?〇〇bu」のようなテンプレートが若干過学習されている傾向が見られたものの、「石」と「ストーン」、「雪」と「スノー」のような日英翻訳という事前知識を必要とするダジャレを検出できていると言える。

BERT+お手製feature+LGBM

各特徴を作成しconcatしたものをLGBMで学習した。データセットAのtestセットに対するclassification_reportを以下に示す。

              precision    recall  f1-score   support
           0       0.95      0.93      0.94      3378
           1       0.99      1.00      0.99     20480
    accuracy                           0.98     20480
   macro avg       0.97      0.96      0.96     20480
weighted avg       0.98      0.98      0.98     20480

BERT単体に比べ、featureを追加した場合に、高い精度でダジャレと非ダジャレが分類できると言える。特にランダムに選ばれたtest setに対しては、recallが1.0となっており、全てのダジャレを検出する事ができた。ダジャレを過剰に抽出していた例を以下に示す。

text    rule_base
職業婦人に生理休暇を!       True
ボン・ボヤージ!       True
露西亜よ汝は飛ぶ       True
ヒューメーンということに就て       False
私が占ひに観て貰つた時       False
わが工夫せるオジヤ       False

実際ルールベースで繰り替えしが判定された上で、文章的に感嘆符が付くなどして勢いがあるもの。もしくは、少し複雑な仮名遣いをしている場合に誤検出してしまう事が見て取れる。

  

データセットB

データセットBに対して、BERT+お手製feature+LGBMを利用し、同様に実験を行った。データセットBのtestセットに対するclassification_reportを以下に示す。

           0       0.93      0.89      0.91      5706
           1       0.94      0.96      0.95      5672
    accuracy                           0.94      3482
   macro avg       0.94      0.93      0.93      3482
weighted avg       0.94      0.94      0.94      3482

実際にルールベースで事前に判定した上でサンプリングしており、誤検出の割合が0.9を下回っていることから、BERTやfeatureが精度に貢献している事がわかる。また、データセット内の偏りがない場合でも、分類性能のあるモデルになっている事がわかる。

 

データセットCおよびD

実空間上のデータへの適応として、データセットCおよびDに対して、データセットAで作成したモデルを適応する。

データセットCでダジャレとして検出された 328 (0.0082)
データセットCでルールベースで検出されなかったがモデルにより検出された 91 (0.0022)
データセットDでダジャレとして検出できた 1998 (0.0104)

ダジャレモデルで検出されるデータは、概ねツイートデータの1%以下となる事がわかった。
また、データセットCで抽出された328件のツイートを目視で確認した。その時ダジャレであると判断できたものは151件であり、目視したうちの46.0%、ツイート全体の0.38%がダジャレであった。データセットDにおいては、事前のルールベースによるデータの偏りがないため、多くが誤検出であった。モデルの学習を対象ドメインにfitするよう工夫する必要がある事もわかった。

このモデルがrecallが高いモデルであることから、筆者の体感に比べ、Twitterでダジャレ言ってる人はかなり少ないということになる。20%は居ると思っていたが。この記事を見ているあなたはもっとユーモアを出して欲しい(you moreだけに)。

 
参考までに実際に抽出されたダジャレツイートの例を以下に示す。
 
クロール期間中、ちょうどユリ・ゲラー氏と任天堂が和解した件で盛り上がっていた。


 
Twitterっぽい。好き。
 
ダジャレbotのような存在も複数検出された。
 
そうだね、プロテインだね。

まとめ

本記事では、既存のルールベースを拡張するだけでなく、NLPを利用した機械学習モデルによるダジャレ判定を行った。また、ダジャレ界隈において今日まで行われていなかったDocker、poetryを利用し、データ以外のスクリプト部分について再現可能な状態を作成した。
github.com

結果として、人工的なデータセットであれば98.9%の精度でダジャレかどうかを判定できた。Twitterのような実世界でも、ルールベースで判定した後であれば、46.0%がダジャレとして見なせるよう抽出できた。一体誰が嬉しいのか全く分からないができた。

副次的に、Twitterでダジャレを言っているユーザが少ない事も示唆された。SNSとしてかなりマシさ(示唆だけに)。

おわりに

このような実験の先に、コントやTV番組、ラジオ、YouTube、日常会話において面白いと感じるポイントの検出や、なぜ人が面白いと感じるのか、面白さは生成できるのか、笑いの歴史ごとの違いは何か、今バズっている面白いものは何か、自分を面白くするにはどうトレーニングすべきか、ダジャレの起源、AIとは、といった課題があったらいい気がする知らんけど。

感想や指摘はTwitterはてブにくれたら見て対応するかも知らんけど。

先行事例を世に送り出した各位に心から謝礼を送りたい(シャレだけに)。

f:id:vaaaaaanquish:20201211051055p:plain:w0

 

*1:kurehajime. 文章からダジャレのみを抜き出すコマンドを作ってみた, https://qiita.com/kurehajime/items/a922d42dff5e0f03d32c, Qiita, 2015/8/27

*2:fujit33. おもしろいダジャレを入力すると布団が吹っ飛ぶ装置を作った, https://qiita.com/fujit33/items/dbfbd7a2aa3858067b6c, Qiita, 2019/1/9

*3:青山学院大学, 谷津 元樹. 音韻類似性を考慮する教師あり機械学習を用いた駄洒落検出環境, GitLab: https://gitlab.com/m-yatsu/djr_wpsm, ニコニココメントデータからの駄洒落検出: https://www.nii.ac.jp/dsc/idr/userforum/startup/IDR-UF2019_S03.pdf, http://arakilab.media.eng.hokudai.ac.jp/~araki/2018/2018-D-11.pdf

*4:谷津元樹, 荒木健治. "駄洒落の面白さにおける要因の分析." 日本知能情報ファジィ学会 講演論文集 第32回ファジィシステムシンポジウム. 2016. https://www.jstage.jst.go.jp/article/fss/32/0/32_237/_article/-char/ja/

*5:ダジャレ TechTalk - エムスリーテックブログ https://www.m3tech.blog/entry/2018/08/03/182447, 2018/8/3

*6:内田ゆず, 荒木健治, "オノマトペに着目した駄洒落の面白さの分析―駄洒落の自動生成に向けて―." 日本知能情報ファジィ学会 第35回ファジィシステムシンポジウム, 2019, https://www.jstage.jst.go.jp/article/fss/35/0/35_332/_article/-char/ja/

*7:KOBAYASHI Yoshitomo. "駄洒落の基本構造と笑い" 東京外国語大学 http://www.tufs.ac.jp/st/personal/03/conanweb/dajare.htm

*8:荒木健治. "駄洒落データベースの構築及び分析" ことば工学研究会: 人工知能学会第 2 種研究会ことば工学研究会 57 (2018): 39-48. http://arakilab.media.eng.hokudai.ac.jp/~araki/2017/2017-C-3.pdf

*9:ブログに書くネタ記事のために学術目的としてデータセット申請するのは私は流石に恐れ多くて無理の助

*10:100字以上のダジャレはfuture workとする

*11:この世のツイートの82%がダジャレであるなら別である

*12:fuzzysearchで使ったLevenshtein距離による類似性とNWによる類似性はequivalentである事が知られている Sellers PH (1974). "On the theory and computation of evolutionary distances". SIAM Journal on Applied Mathematics. 26 (4): 787–793. doi:10.1137/0126070. https://epubs.siam.org/doi/10.1137/0126070

*13:Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks,Nils Reimers, Iryna Gurevych, EMNLP 2019, https://arxiv.org/abs/1908.10084

*14:筆者は最近何も考えずsentence bertに入れて目的のタスクが解けそうか試してからNLPタスクを始める事が多い

*15:【日本語モデル付き】2020年に自然言語処理をする人にお勧めしたい文ベクトルモデル - Qiita https://qiita.com/sonoisa/items/1df94d0a98cd4f209051

*16:Maaten, Laurens van der, and Geoffrey Hinton. "Visualizing data using t-SNE." Journal of machine learning research 9.Nov (2008): 2579-2605. https://lvdmaaten.github.io/publications/papers/JMLR_2008.pdf

おすすめのエンジニアリング関連YouTubeを紹介しながらエンジニアとYouTubeについて考える

- はじめに -

最近、所属企業でYouTubeの企画やインタビューを進める事になった。
私の所属する企業は、どのような事柄に対しても説明責任を重視する企業であり、ある程度の合理的な理由付けの上でYouTube上での広報活動をしていこうとなったのだが、実際は「Podcastで良いんじゃないか?」「ブログとリーチできる層は違うのか?」という話が後からも出てくる事が予想できるので、自分の中でも整理と記録を取っておきたい。

 
体感として、特にソフトウェアエンジニアリング業界でのYouTubeに対する評価は、正直半々といった所だろう。
私の認識としては、大きな2つの主張を短く要約すると「YouTube(全般的に)は面白い」「日本のソフトウェアエンジニアのYouTuberが技術の話をしていない」辺りにまとめられる。
  
これはある種実態を表しているとも言えるし、違うとも言える。
 

この事も踏まえ、本記事では、私が日常的に見ているソフトウェアエンジニアリング関連の日本のYouTubeチャンネルを挙げながら、YouTubeの特性や期待効果についてまとめつつ、私にとってどういう技術動画が増えたら面白いかだけを考える記事である。

 
前提として、本記事は、「技術の話をしないエンジニアYouTuber」について触れはするものの、そういったアカウントを紹介するものではない。また、私の企画を考えるための整理なので、海外の技術Youtuberには極力触れない。私個人の考えであって、会社の意思決定を解説したものでもあるべき論でもない。


 

- なぜ技術と動画なのか -

そもそも動画×ソフトウェア技術というのが昔から相性が良かった事はある程度自明である。

例えば、ニコニコ動画全盛期からプログラムした物理エンジンの動画を投稿し、今も活動しているむにむに先生。
www.youtube.com
www.youtube.com

ニコニコ動画全盛期に見たことがある人も多いはず。Youtubeの動画もあいも変わらず面白いので是非見て欲しい。
私自身、ニコニコ動画全盛期、高専の課題の一貫でゲームをプログラムで攻略する動画を作り投稿したりしたものだ。
プログラミングでここまで面白い動画を作り続けられる人といいうのもなかなかいない。

 

同じ具体的な結果が見えやすいゲーム制作のYoutuberも既に幾人も居る。
www.youtube.com
特に私もたまにライブストリーミングを見ているHytackaのゲーム制作チャンネルは、ソフトウェアプログラミングの概念からは少し広がるものの、Unreal Engineを使ったGUIプログラミングによって、実際のゲーム開発社が「どのような思想で」「どのようなユーザ心理を想定して」ものづくりをしているかが見えてかなり面白い。

 

動画であることで、技術ブログ等の文字では伝わらない微妙なニュアンスが伝わる。
「このクッキーおいしいよ」という文面から、本気なのか嘘なのか嫌味なのか分からない所が、音声と映像になって情報量が増えることで把握しやすくなるといった具合だ。
また、「作業工程や結果を見せる」という側面でも動画の良さはあると言える。
文字から知ることができない細かな画面操作やノウハウが、時に自分の効率化にハマる要素になり得る。

 

上記のようなYoutubeチャンネルは、どうしても扱うテーマ故に「プログラミングの結果」を見せる側面が大きいと言える。
これだけでは「いやいや、プログラミングは〜」とソースコードをカチャカチャする画面を流すYoutubeチャンネルについての考察が出てくるのは自然であろうから、それらも含めて考えてみる。

 

- どんなエンジニアリングのYoutube動画が面白いと感じるのか -

人間の面白く感じるポイントは千差万別である。
以下は、それらを否定するものではなく、「私自身がソフトウェアエンジニアとして面白いと感じて見てしまう動画」をそれぞれ言語化するだけのものである。

 

主に技術関連の動画の面白さの要点は以下に分けられると私は考えている。

  • 内容が(かなり)勉強になる
  • 見知った人が出ていいる
  • 出ている本人達が楽しそう
  • 出てくる結果が面白い
  • プログラミングを生かした企画自体が面白い
  • 属性が面白い

以下では、この面白さそれぞれを深ぼってみる。

 

内容が(かなり)勉強になる

最も一番に出てくるのは技術的な深み、面白さだと思う。

例えば、個人で再生数の多い所で言えばSatoru Takeuchi氏のチャンネル。記事執筆時点で8割見ていた。
www.youtube.com
形式としてはスライドと実演の2パターンで、大学の講義のような形式。丁寧で内容を考えているのがすごくわかる。
www.youtube.com

同じ形式では徳丸先生。
www.youtube.com

Takami Sato氏のKaggle解説なども動画が上がるのが楽しみである。
www.youtube.com

 

この手の講義に近い動画は、暇つぶしに見ていても悪くないし勉強になる一方で、ライバルも多い。
「企業チャンネル」「テックカンファレンスチャンネル」「大学・研究機関チャンネル」辺りが犇めき合っているので、時間の奪い合いないし、「後で見るを押すけど見ない」という過多状態にもなりがちだと思う(私自身後で見るの数がヤバい)。

NIIだって、東京大学だってチャンネルを持って講義に近い動画を上げてくれている。特定のLabが1チャンネルを持っているパターンもある。
国立情報学研究所 - National Institute of Informatics - YouTube
東京大学 / The University of Tokyo - YouTube
STAIR Lab - YouTube
今のYoutubeの自動翻訳機能があれば海外の大学も…となる。


各言語やフレームワークのConferenceは今や年から年中開催されていて、そのアーカイブがネットに上がる。
PyConJP - YouTubejsconfjp - YouTubebuilderscon - YouTubeyapcasia - YouTubeVue.js日本ユーザーグループ - YouTubehtml5j - YouTubePHPer Kaigi - YouTube、...


IT企業がチャンネルを持っている場合も多い。
Google Cloud Japan - YouTube
日本マイクロソフト株式会社 公式チャンネル - YouTube
NVIDIA JAPAN - YouTube
Cookpad - YouTube
Cybozu Inside Out - YouTube
クラウドエース株式会社 Cloud Ace, Inc. - YouTube
クラウド会計ソフト freee(フリー) - YouTube
DeNA Channel - YouTube
mercari devjp - YouTube
mixi Tech Talk - YouTube

IT企業チャンネルの場合は、講義のような登壇、プレゼン形式の他にも戦略的に色々な企画を出している所が違いになる。
例えば、メルカリ、mixiが動画にしているライブコーディング等は、学び以外にも副次的な効果を幾つか狙っていそうな動画になっている。
www.youtube.com
www.youtube.com
文字では伝えづらい微妙な言葉のやり取り含め、社内の和気藹々とした雰囲気、開発のスタイルを外に伝える事で採用・広報とする形だ。
業務の内容やPC画面など、セキュリティの課題をクリアにさえできれば、プレゼンスタイルの技術動画の間にこういった動画が挟める事は長期的にも良い効果があるだろう。採用の他、社内コミュニケーション増、愛社精神の向上、技術力向上、キャリア辺りがそこに当たる。
後述する「見知った人が出ていいる」「出ている本人達が楽しそう」といった面白さも含めて、良さそうである。

一方、企業のYoutubeチャンネルは、営業に使う資料置き場になっていたり、淡々とプレゼンを上げて視聴者数で悩むパターンもありそうなので、後ろ盾がない分個人やカンファレンス、大学の動画の方が長期的な勉強という側面での価値を生みやすいと言えるかもしれない。
 


その他細かいが、より基礎から学びたい場合は、プログラミング教室の講義動画や塾講師等もライバルに入る。講義動画はよもや可処分時間の奪い合い。戦国時代だ。
このような状況なので、自分の少し上の技術情報、レベルを上げてくれる技術情報の方がウケそうではある(技術以外のトークは別として)。

 

見知った人が出ている

これは、「技術動画の面白さとして本末転倒ではないか」という議論の余地こそあるものの、正直面白い。

「技術の話をしないエンジニアYouTuber」もある種この「見知った人が出ている」から面白い所の極端な形であるとも言える。
例えば私が最近真似したいなと思うのがForkwellのチャンネル。
www.youtube.com

「開発者向けニュースピックラジオ」と銘打って、ITに関連した話題に対して特定の分野では名が通るようなエンジニアが話すという内容。例えば最新の動画は「日本Node.jsユーザグループ代表 古川陽介氏」などがゲストだ。高い低いに関わらず、自分と違ったレイヤの人が同じコンテンツに深い知識で語るのは見ていて面白いし、何より「その人の背景を知っている」ので頷きやすい。正直よく分からない人がやっても面白いと感じるのは難しい内容だと思う。


他に見知った人が出て語っているもので、好きなのはBIT VALLYのチャンネル。
BIT VALLY自体は同僚の登壇で知ったが、直近は過去の動画をラジオ代わりに聞きながら仕事を進めていた。
www.youtube.com

以下の回は特に豪華な人達なので再生数もうちょっと伸びても良いと思う。
www.youtube.com


さらに小さい物まで見ていけば、フォロワーがやっているコーディング放送、技術解説動画などは技術的な深みに関わらず、少し見ようと思ってしまう。技術に関わらず、「面白い事」「話題性がある事」は欠かせない要素の1つであると言える(それが全てではないが10分の1くらいはこれ)。

 

出ている本人達が楽しそう

これもまた「技術動画の面白さ」とは少しズレるが、技術動画として大事な要素の1つであると感じる。

楽しそうにアセンブリを触る人達。
www.youtube.com

楽しそうに電源コードで朝食を作る人。
www.youtube.com

面白く工学を紹介できる人の方が、やっぱり見続けられるし同じ内容でも見ていて楽しい。
「難しい問題に悩んで楽しい」「工学の純粋な部分が楽しい」など場面は様々であるものの、楽しい所を楽しく見せられる動画は良い。本当にかくありたい。

 

出てくる結果が面白い

「作ってみた」の面白さも大事な要素になり得る。

なんと言ってもプログラミングやら機械いじりは、絵面が地味である。
これは変えられようがないので、「考える」「とにかく動かす」ところに比重を置いて、プログラミングを軽いものにしていくパターンである。

それこそ、冒頭のむにむに先生が、昔からやってみたの動画を投稿している人として有名である。

他で言えば、更新間隔こそ長いが、ゲヱム道館というチャンネルは上手くやっているなと思う。
youtu.be

プログラミングの解説は短く丁寧に、CLI上に結果を表示して解説もしている。
「結果が見えるか見えないか」というのは、ここでは大きな体験の違いになる。

しかもゲヱム道館さん、ロケハンをしている。偉い…。
準備だけで相応の時間が掛かっていそうだ。パックマンファイアーエムブレムドラクエ等を作ってみる動画も見て、良いシリーズだなと感じる。

 

プログラミングを生かした企画自体が面白い

プログラミングを生かして、地味な絵面ではなく、ためにもなって面白い企画。
Youtubeやる上でもかなり強い要素である。

私が好きなのは、じょえチャンネル。競技プログラミングの問題を目隠しで解いたり、コーディング面接と称してNP完全の問題を解かせたり、プログラミングを活かしながら笑える。
www.youtube.com
正直かなり好きなので、この勢いでAtCoder Liveと突然のコラボとか、更に面白企画でチャレンジして欲しい。

関連したアイデアは結構あって、「GoogleなしでTwitterや質問サイトで質問するだけでプログラミング」とか「ドメイン知識が凄い人と一緒に分析問題を解く」とかゲームさんぽ*1よろしく、何かのプロとプログラミングをかけ合わせたりするのも面白いと思う。

企画が面白ければ、新規参入者も見込めるし、チャンネル登録者は動画投稿のモチベーションになり得るので、一辺倒にならず色々やるのが大事なのは自明。

 

属性が面白い

動画投稿者の属性も最後は大事になる。
例えば、VTuberの技術関連動画も少なくない。

AIcia Solid Projectチャンネルは、その中でもかなり大きい。現時点で登録者が1.3万人、内容は「機械学習」「統計」「線形代数」辺りである。
www.youtube.com
講義のような動画と比較して全てが事細かく具体的という訳にはいかないが、丁寧に論文引用をするだけでなくシリーズを見ていけば理解しやすくなる構成になっている。何より解説のための編集がすごい。一体何者なんだ…

 
www.youtube.com
www.youtube.com
www.youtube.com
日経クロステックがやっている。すごい世界観だ。
あまりVTuberの動画を見ている訳ではないが、Xmoaチャンネルは取り上げる題材も面白いので、YouTubeのトップでレコメンドされてきたらついつい見てしまう。

VTuberになる技術が一般化してきた証でもあり、それに対する障壁もかなり下がっているということでもある。

先に書いた通り、この手の動画コンテンツはライバルが多いため、属性や動画自体の品質、ストーリー性で勝負する事も一つの方法であると感じる。


 

エンジニアとYouTube

ここまで、私がチャンネル登録しているYouTubeチャンネルを挙げながら、エンジニアリング関連動画の面白さについて考え、まとめた。

もちろん動画は、良い点ばかりではなく、テキストと違い「コピー・ペーストができない」「箇条書きのようなリストで見る側の意識が飛びやすい」「編集が大変」といった課題が山積みの分野ではある。
一方で、先のプログラミング関連動画などは、リモート社会になる以前、各人が会社の中で教える事で伝えてきたような細かなニュアンス、技術、社会人としてのテクニックが盛り込まれており、肌で感じられる度合いが高いのは分かる人が多いと思う。今後もリモート前提の働き方、勉強会、技術カンファレンス、学会が続く中で、テキストと両者の良いところを切り替えながら、利用していければと思うところである。
諸問題もエンジニアリングで解決されていくだろうし。


何より、技術の表現の仕方や吸収の仕方自体が多様になるのは還元すべきだと私は思う。

おわりに

他にもオススメのYouTubeチャンネルあったらおしえてください

 

追記

*1:医師やデザイナ、気象予報士とゲームを実況しながら遊ぶチャンネル