seri::diary

プログラミングのこととかポエムとか

システム運用の現場でしか学べないことは他メンバーに積極的に経験してもらうべきだった

基本的に自分はタスクを拾いすぎてしまう傾向にある。それに加えて比較的朝型なこともあり、前職ではエンジニアの中で一番朝早く出社していることも多かった。*1

その結果どうなるかというと、朝出社して見つけた運用上のトラブルは大体自分がとりあえず手を付ける状態になっていた。前日の夜間バッチやその日の早朝に動くバッチがコケて問い合わせが来ているのでそのリカバリをする、前日にデプロイした後レスポンスが高くなってアラートが出ているのでその調査をする、web appがやたらと500系エラーを吐いているのでBugsnagを見る、等々。

出社している以上無視するわけにもいかないというのもあるが、見つけてしまうと放っておけない性格ということもあり最優先でこれらの対応をしてしまっていた。お陰で前職で触っていたproductについてはかなり広範囲の知見があり、その行動がそれなりに社内での評価につながっていたのではないかと思われるのだが、一方で今はその行動については後悔している。

そういう球拾いを自分だけがやりまくっていた結果、何が起きたかというと、自分しか対処できないトラブルのようなものが増えてしまった。そしてその状態でそのチームから離れてしまった。もちろん引き継ぎ資料は大量に書いたし、自分が何か早朝とか深夜とかに対応した日は、その日何があってどういう対応をしたかはesaなどに障害内容と作業内容を決められたフォーマットで記載して貯める運用をしていたし、それなりに大事であればその日のうちにmtgをセッティングして口頭でサーバーサイドチーム内で説明した。知見を共有するという点では出来る限りのことをやったという自負はある。しかし、SREなどをしていてシステムの運用を経験したことがある人なら分かると思うが、障害対応において「知っている」ということと「やったことがある」というのでは経験値にかなりの差がある。自分は他のメンバーが「やってみる」という経験を大分奪ってしまっていたと思う。 早朝や深夜では自分に限らず気づいた人間が真っ先にやるしかないのだが、それでも、もし自分の作業中に出社してきたら一緒に調査するとか、実際にやった対応をステージング環境とかで一緒にやってみるとか、もう少し実践的な経験を積ませるようなことを意識すべきだったと思う。人が少ないベンチャーで常に余裕がない状態ではあったが、人が少ないからこそ、現場で学ぶことが一番の近道であるようなことを経験してもらってレベルアップしてもらうべきだった。新しい言語やライブラリの使い方はいくらでも好きな時間に学べるが、いざシステムで障害が起きた時の対応方法やその後のリカバリはいくらマニュアルを作ってそれを学んでもらってもカバーしきれないところがある。

だからこそ、普段から知識だけでなく機会をシェアできるような運用の仕組みを作らないといけないと感じる。例えば、以前、受託開発をしていた時代に常駐していた客先では、社内からの問い合わせを受け付ける窓口のエンジニアを当番制で決めており、何かあったらまずはその当番のエンジニアが調査を行うという仕組みを作っていた。この仕組は非常によくワークしていて、プロパーだろうがSESだろうが新人だろうがベテランだろうが強制的にアサインされるため強制的に知見を吸収する機会を作ることができる。こういう機会を設けて、組織として運用経験をシェアするような仕組みが必要だと感じる。インフラエンジニアがいるから安心ということは全くなく、むしろサーバーサイドに関わるエンジニアがSQLを書いたりhotfixを出さないと治らない障害の方が圧倒的に多い訳なので、専用のSREチームを構築できないような規模の組織では(多分そのケースがほとんどだと思うが)サーバーサイドエンジニアが運用知見をどれだけ持っているかが運用において重要だと今にして思う。

*1:多分9時前には大体出社していたし、忙しい時は7時台ということもあった。

春学期に読んだ論文まとめ

といってもあまり読めてない気がする。6月以降は完全にレポートに忙殺された。

とりあえず読んだ順に紹介。

深層学習関連

TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems

TensorFlowのwhite paper。TensorFlowの歴史、google社内での利用用途、計算グラフの構築と実装、parameter serverを使った並列訓練の話などかなり多紀にわたってtensorflowについて紹介されている。

今でこそdefine by runのアプローチが柔軟で良いとされる風潮にある気がするが、この論文を読んだ限りではモデルの表現の柔軟さと実装時の最適化を両立できるdefine and runのアプローチも悪くないのではないかという気がする。論文によればTensorflowは同一の計算グラフでモバイルデバイスからサーバ上でまで同じコードで実行できるとあり、実際Androidでも学習済みモデルを使った推論が可能だし、MobileNets というモバイル用に省メモリで動作できるcNNも提案されている。広いプラットフォームを持つgoogleとしては実行とネットワークの定義を切り離すことは会社としても重要な戦略であったと考えられる。

Large Scale Distributed Deep Networks

TensorFlowの前身であるDistBeliefという機械学習フレームワークの紹介。こちらはwhite paperというよりは、parameter serverを用いた分散深層学習のアルゴリズムとそれを実現するシステムアーキテクチャの話題が中心である。

ここで扱っているアルゴリズムは非同期に勾配を更新するDownpour SGDと、バッチ処理で非同期に勾配を更新するSandblaster L-BFGSである。正直、両者の根本的な違いがよく分からんかったのだが、前者はパラメータープロセスは1台で、後者はパラメータサーバを複数台に分散し、それらに指示を出すコーディネータープロセスが全体の処理を管理する点が異なるらしい。

ChainerMN: Scalable Distributed Deep Learning Framework

日頃お世話になっているChainerの分散プロセス実行ができるように拡張してくれるライブラリのChainerMNについてのwhite paperである。 書かれている内容はChainerMNの紹介がメインだが、2章のPreliminariesで深層学習、分散深層学習についての解説がとても細かく載っているので、ここを読むだけで深層学習と分散深層学習についての基本的な知識を得られると思う。データ並列とモデル並列の図解も*1とてもわかりやすい。

A Data and Model-Parallel, Distributed and Scalable Framework for Training of Deep Networks in Apache Spark

Apache Spark上で動作するモデル並列訓練を行うための分散深層学習フレームワークの紹介。 実装はspark上で行列計算にJBlas、自然言語処理用にMalletを使用して全結合NN、cNN、LSTMを実装した、とあるが、コードが全く出てこないのでどのように実装されたのかよくわからない。

パラメータの更新には独自の分散バックプロパゲーションを考案したとあり、これがこの論文の貢献のようなのだが、数式を見る限りはどの辺が新しいバックプロパゲーションなのかよく分からなかった。。モデル分散してるから行列を列単位に分散してるのでバックプロパゲーションの数式がそういう風に変わるよねというのは分かるが、何か自分が見落としてるのかもしれない。。

AMPNet: Asynchronous Model-Parallel Training for Dynamic Neural Networks

これもモデル並列を行うための分散深層学習フレームワークを作ったぜという話。ただ、これはモデル分散といっても処理対象のパラメータの行列を分割するようなものではなく、レイヤーごとに別のノードに配置してパイプラインで処理を行うというアプローチ。

途中で読むのを辞めてしまったので若干正確性に乏しいが、深層学習の訓練においてはメモリからデータをロードする部分がボトルネックになったりGPUのプロセッサがボトルネックになったりとハードウェア的に処理の負荷がかかる箇所が計算フェーズによって異なるという事情があるのに対し、既存のフレームワークは一切それを考慮しておらず、ハードウェアを使い切れていないということを批判している。*2

そこで、レイヤーごとに実行するノードを分割し、1 iterationをパイプラインに分割してそれぞれの処理を別々のマシンで行うことで、各レイヤーの計算に特化したハードウェアを詰むことでハードウェアを使いきれるようになるね、という主張らしい。

Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcastin

レーダーエコーで計測した降水データを時空間予測問題として解くための畳み込みLSTMを提案する論文。要するに動画のフレーム予測と同じようなアプローチで降水予測を行おうというアプローチである。

降水データは6分ごとの観測点の降水強度を行列にマッピングし、それを時系列的に連続する20枚のフレームを1 windowとして、最初の5frameをinput、残り15frameを予測する問題設定としている。

ネットワーク構成がユニークで、2層のLSTMレイヤーでencodingした後にそのレイヤーをコピーした別のレイヤーで予測を行い、inputと同じ行列サイズのアウトプットになるように畳み込んで予測フレームを出力している。なぜこのような構成にしたかは不明。あとなぜかloss関数がSoftmaxである。

専攻研究であるReal-time Optical flow by Variational methods for Echoes of Radar(ROVER)という数理モデルを用いた手法よりも高い予測精度を示した。

なお、この論文を参考に気象庁が提供するレーダーエコーデータを用いて同じようにConvLSTMで訓練を行ってみたが論文ほどの精度は出なかった。やはり10分毎計測、1km間隔の観測データしかないので、動画のように扱って予測するにはちょっと解像度的に厳しいかもしれない。

その他

Software model checking

とある講義のレポート課題で読んだやつ。 ソフトウェアモデル検査に関する研究についてまとめたサーベイ論文である。述語論理の用語が全然分からなくて訳すのすら大変だった記憶。

Halide: A Language and Compiler for Optimizing Parallelism, Locality, and Recomputation in Image Processing Pipelines

これも↑と同じ講義でレポート課題になっていたので読んだやつ。

最近一部で静かなブームとなりつつある、ハイパフォーマンスなコードを生成するコンパイラと記述するためのDSLを提供するHalide。Adobeの画像処理エンジンなどで使用されているらしく、この論文ではどのようにして画像処理一般で行うstencil計算をDSLでシンプルに記述してコンパイラで高速化したコードを吐くか、という手法について書かれている。Halideで生成されたコードと人間が手でチューニングしたコードのパフォーマンス比較もしているが、この結果がかなり凄くて、Halide DSLで34行で書いたコードが人間が手で書いた122行のCのコードよりも4.4倍高速だったりする。またcudaを使用したコードも出力でき、そちらでも人間が書いたどのコードよりも早いという結果になっている。人間が書いたアセンブラよりもccが吐くアセンブラの方が速い!みたいな感じだろうか。

面白いのでもう少し個人的にいじってみたい所存である。なお、Halideコンパイラを用いたコード高速最適化フレームワークTiramisuというやつもある。こっちも気になる。

*1:これはPFNの中の人のスライドでよく見るやつだが

*2:実際これはnVidia visual profilerとかでprofileしてみると分かるのだが、モデルサイズが大きいネットワークを回すとボトルネックになっているのはCPUからGPUへのデータ転送だったりしてGPUのプロセッサは50%も使ってなかったりするケースがある。これは実際に自分が実験した確認した実例である。

分散深層ニューラルネットワークの実装アプローチまとめ(2018年6月版)

これは何か

  • 自分が研究テーマとして扱っている分散深層ニューラルネットワークには、「分散」処理の部分において複数のアプローチが存在する
  • このエントリでは、自分の知識の整理のためにこれまで調べたことをまとめておく
  • (2018年6月版) と書いたのは、深層学習業界は変化が激しすぎて半年後には状況が変わっていてこのエントリが役に立たなくなることを想定しているためである(その時はまた新しい版を書こうかなと)

分散深層ニューラルネットワークとは

一般に「深層学習」と呼ばれる機械学習手法においては、隠れ層が多数連なる「深層ニューラルネットワーク」が使用される。
昨今では隠れ層の数を大規模に増加させて学習を行う手法が、主に画像認識の分野で効果を上げており*1、それに伴って訓練に要する時間も増加傾向にある。この問題を解決するために、スループットを向上させるためのアプローチとして分散処理を深層ニューラルネットワークに導入する手法が注目されている。

分散深層ニューラルネットワークでは大きくわけて2つのアプローチ、データ並列分散訓練モデル並列分散訓練 が存在する。以下、それぞれについて解説する。

データ並列分散訓練

f:id:serihiro:20180602214523p:plain

同一のネットワークの複製を複数用意し、それぞれのネットワーク(以下レプリカと呼称)に対して訓練データを適用して並列に訓練を行うアプローチである。 各レプリカは、通常のニューラルネットワークと同様に訓練を行い、個別に勾配を計算して重み行列やフィルタなどのモデルパラメータを更新する。

ここまでは並列でない通常のニューラルネットワークと変わらないが、データ並列分散訓練においては、各レプリカの訓練で得た勾配を、数iterationごと、あるいは毎iterationごとに集約して、平均化したもので各レプリカの勾配を置き変える。これにより、訓練データを分割して並列に訓練を行いつつも、逐次実行で全訓練データを用いて訓練したかのような結果を得ることができる。

この際、レプリカ間で訓練に用いる勾配の「鮮度」が課題となる。常に最新の(つまり鮮度が高い)勾配を用いて訓練を行うには、毎iterationごとにレプリカ間で処理同期を取った上で、勾配を集計し同期する必要がある。しかし、この集計処理がボトルネックとなり、ネットワークのスループット低下につながる。

一方で、勾配の更新頻度を下げすぎると、勾配が同期されるまでは、各レプリカ上のローカルな勾配を用いて訓練が進み、勾配の「鮮度」が劣化する。これにより、レプリカ間のモデルパラメータの差異が大きくなり、損失の収束を遅らせるなどの悪影響をもたらすという問題が生じる。

実際の深層学習フレームワークにおいては、同期処理によるスループット低下を防ぐために、非同期でレプリカ毎に異なるタイミングで勾配を更新する手法を採用しているものもある。以下、同期、非同期それぞれの手法について解説する。

勾配の同期更新

f:id:serihiro:20180602215407p:plain

ニューラルネットワークにおいて、勾配は訓練におけるBackwardフェーズでレイヤー毎に計算される。そして求めた勾配に学習係数をかけた値を使ってモデルパラメータを更新するのが一般的な手法である。

勾配の同期更新においては、モデルパラメータを更新する前に、レプリカ間で同期を取り、各レプリカで求めた勾配の平均値(以下、平均勾配と呼称)を用いて全パラメータを更新する。つまり、各レプリカで計算した勾配をモデルパラメータの更新に用いるのではなく、平均勾配を用いて各レプリカのモデルパラメータを更新する。これにより、常に最新の平均勾配を用いて訓練を行うことができる。

このアプローチは、具体的なプロダクトとしてはChainerMNが採用している。バージョン1.3では、同期更新時に他の処理と一部オーバーラップさせることで同期による処理遅延を低下させる DOUBLE BUFFERING という機能が搭載された。この機能を使うと、一時的に古い勾配のまま訓練を行うワーカーが生じるが、精度には影響のないレベルであるとのことである。 *2

勾配の非同期更新

f:id:serihiro:20180602221811p:plain

非同期に更新を行う場合、各レプリカ間で同期を取らずに平均勾配を求めるため、勾配の集約と平均勾配の計算を行うための別のプロセスが必要である。かつてGoogle社内で使われていたDistBeliefという深層学習フレームワークにおいては、パラメーターサーバという専用プロセスがこの役割を果たしている。Googleのプロダクトとしては後発であるTensorFlowを分散実行させる場合においてもこのアプローチは同じようではある。

非同期更新においては、各レプリカが求めた勾配をパラメーターサーバに送信し、その勾配を用いてパラメータサーバが平均勾配を更新する。各レプリカは次のiterationの訓練を開始する前に、パラメーターサーバから最新の鮮度の高い勾配を取得し、それを用いて訓練を行う。

各レプリカは同期を取らずに平均勾配が更新されるため、レプリカ間で使用している勾配に差が生じている状態が発生する。このことにより、より良い勾配を古い勾配を使った訓練により生じたより劣悪な勾配によって、悪化させてしまう事も考えられる。 このような最新のパラメーターサーバーにある勾配と比べて古い勾配を「陳腐化した勾配(Stale gradient)」と呼ぶ。陳腐化した勾配の影響を緩和するには、学習率を下げる、陳腐化した勾配を定期的に捨てる、ミニバッチサイズを調整する、等が考えられる。*3

モデル並列

f:id:serihiro:20180602220457p:plain

レプリカを作らず、ネットワーク上のパラメータを複数のプロセスに分割し訓練を行う並列化手法。 これにより、例えば前結合ネットワークにおいて、巨大な行列となった重み行列と入力ベクトルとの計算を複数のマシンで分散して実行することで、要求されるマシンスペック要求の低下*4と、並列化による高速化が望める。

一方で、ネットワーク的に分断されたマシンクラスタで実現する場合、各プロセス間のデータ通信が大きなボトルネックになると考えられる。そのため、全結合ネットワークやcNNではスループットが大きく低下する可能性が考えられる。この手法を適用して高速化を行うには各プロセス上での高い密度を高くすることができるネットワークが適していると考えられる。例えば、TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systemsではモデル並列の実装例としてLSTMを挙げている。

自分が論文を調べた限りでは、Apache Spark上に全結合ネットワークやcNNをモデル並列で実際に実装している例*5や、レイヤーごとに複数のマシンに分割し、訓練処理をパイプライン化して並列効率を向上させる実装*6もあるが、まだ実例が少ないアプローチであると考えられる。

2018年6月時点では同期更新・非同期更新どちらが良いのか

様々な条件下で両者のアプローチを比較した2016年の論文Revisiting Distributed Synchronous SGDによると、同期更新の方が非同期更新よりも収束速度も正解精度も高いことを示している。また、訓練を実行するワーカー数を増加させた場合も同期更新の方が収束速度がより効率的にスケールすることを示している。近年の画像コンペにおいても上位入賞者チームにおいて同期型が支持されているという意見もある。*7

よって、非同期型に特化した新しい手法が開発されたら状況は変わる可能性はあるが、2018年6月現在では同期型の方を選択する方が正解である可能性が高い。例えば2017年9月にarXivに投稿されたImageNetの訓練に関する論文、ImageNet Training in Minutesでも、同期型データ並列アプローチを使っていることが明記されている。

まとめ

訓練精度という観点で見ると、データ並列・同期更新が現時点では最も高い精度を得つつ、高いパフォーマンスを発揮できるアプローチだと考えられる。しかし、メモリに乗り切らない大規模なパラメータを扱う場合や、マシンリソースの有効活用という観点では、モデル並列を活用できるシーンもあると考えられる。*8

実際、自分は大規模なパラメータを想定したネットワークにおいてモデル並列訓練を行った場合の性能特性について研究する予定であり、現在c++Blas系ライブラリとMPIで、モデル並列ネットワークを実装している最中である(作るのはとりあえず全結合ネットワークのみ)。自分の研究成果については追ってまた別のエントリで紹介したい。

参考文献

*1:例えばここ数年の画像コンペでかなり使われているcNNの1つであるResNetは、作者らの報告によるとImageNetで152層のネットワーク、CIPHER-10で1202層のネットワークを構築した報告がある

*2:https://chainer.org/general/2018/05/25/chainermn-v1-3.html

*3:https://www.oreilly.co.jp/books/9784873118345/

*4:例えば大規模な行列計算を行うノードとそうでないノードとでGPUの搭載数を変えたりすることで、スペックを要求されない部分には低スペックなマシンを採用することができる

*5:https://arxiv.org/abs/1708.05840

*6:https://arxiv.org/abs/1705.09786

*7:https://logmi.jp/285424

*8:完全に余談だが、過去に非同期処理を多用するマイクロサービスを開発してきた元ウェブアプリケーションエンジニアの感想として、DistBeleifに代表されるパラメータサーバを用いたアーキテクチャの方が馴染みがあるので作ってみたい気持ちにはなる。

outputは最大のinput

outputがないとinputできない

仕事でも勉強でも、日々何かをinput ∈ {調べる, 勉強する, 調査する, 聞いてみる, まとめる, やってみる} しないといけないケースが多い。今は学生なので日々やってることの9割はinputである。

ただ、他の人はどうかは知らないが、自分にはoutputのイメージがないとinputが続かないという問題がある。outputのイメージがないと途中で迷走してやる気がなくなってやめてしまったりすることが多い。

例えば「とりあえずScala勉強してみよう」みたいなのがすごく苦手で、具体的な成果として何をoutputするかが決まってないと続かない。例えば「とりあえずPlay Frameworkのチュートリアルやる」みたいなのがすごく苦手で、大抵途中で飽きてしまう。「今度Play使ったプロダクトの開発するからPlayチュートリアルやってCRUD一式を持つweb appを自分で作って概要を知る」とか、「こういう設計をしたいけど分からないので類似プロダクトのコードを読んで設計を理解して自分のプロダクトに反映させる」みたいな感じでないと続かないタイプである。

なので何も考えずに勉強するだけ、というのもすごく苦手である。そのため、今取ってる講義は全部何らかの応用目的があるものだけに絞っている。とりあえず直近で使わないものは全部「競プロに使えそう」ぐらいなのだが、それでも「ただ単位のため」という目的しかないよりは遥かにマシである。

ささやかでもoutputしながらinputする

自分が普段採用しているスタイルはこれである。*1 以下は研究室で使っているpukiwikiに自分が調べたことをまとめているページである。 基本的に備忘録としてoutputしている感じだが、それ以外にも研究室の定例ミーティングで進捗を報告するときに「XXXについて調べました」というような報告をするときに、当該ページのURLを貼って報告する、という使い方をしている。

f:id:serihiro:20180602091248p:plain

あと今気づいたがMPIとかBLASみたいな、個別の研究に影響しない一般的な内容についてはあとで編集しなおしてQiita辺りに投稿してもいいかもしれない。

同様に論文も要約を書きながら読んでいる。

f:id:serihiro:20180602092142p:plain

論文をまとめるフォーマットはいろいろあるのだが、とりあえず自分の場合は以下のようにまとめている。

  • 3行でまとめると
  • この論文が批判していること
  • この論文の貢献
  • 以下要旨(ここはフォーマット決めてない)

f:id:serihiro:20180602092545p:plain

*1:もちろん具体的なソフトウェアを書くことがoutputの場合もあるが、直近だと具体的なブツが公開できてないので今回は説明を割愛。

大学院修士課程に入学した

tl; dr

  • 会社員辞めて2018年4月から筑波大学のCS専攻の大学院修士課程に入学した
  • 分散深層学習の学習高速化とか大規模inputデータへの対応などに関する研究をする予定
  • がんばるぞい

大学院に入学した

2018年4月から仕事を辞めて筑波大学のシステム工学研究科コンピュータサイエンス専攻の修士課程で大学院生をやっている。
働きながら入学手続きしたり引っ越ししたりと色々やっていたので3月、4月はずっとバタバタしていたのだが、ようやく落ち着いてきたので近況についてまとめておこうと思う。

背景

一応5年ぐらいソフトウェアを書いて仕事をしてきた訳だけど、自分はCSの学位を持ってる訳でもなく、ちゃんとCSの基礎的な勉強したことがなかった。*1

そのせいか、業務要件をコードに落とし込むことはできても、「なぜこのアルゴリズムを使っているこっちのライブラリの方が早いのか」「こういうのを早くするためにはどういう最適化をすべきなのか」というような、技術に特化した課題に直面した時に、何もできないタイミングがちょくちょくあった。

特に、サーバーサイドで動作するバッチのパフォーマンスチューニングの面で、「とりあえずプロセスを増やせばスケールできるように設計したが、本当はもっと実装を最適化して必要なサーバ台数を減らすための工夫ができるのではないか?」ということが疑われるケースにおいて、自分の持っている知識の範囲では何もできないケースが多く、これはレベルの低いエンジニアリングだと思っていた。他にもいくつか似たようなケースに直面したこともあって、ソフトウェアのパフォーマンスをエンジニアリングの観点から改善できないことが自分の壁だと認識するようになった。

この壁を何とかしなければ自分のエンジニアとしての未来はないと思えた。ありとあらゆるものがサービス化される昨今で、今自分が作っているAPIやバッチすらも、GUIでモデルやパイプラインを定義して誰でも作れるようになるという予感もあったので、ウェブアプリケーションからクライアントにjson返すしか能がない自分はいつか職を失うだろうという危機感があった。

この壁を突破し、エンジニアとしての仕事を維持していくために自分に今何が足りないか?と考えると、明らかにインプットが足りていなかった。ソフトウェアというものを現場でのみ学んできた自分にとって、経験したことがない問題に対してほとんど何もできないという弱点があることに気づいた。その弱点が露骨に表れるケースの1つが前述のような大規模データの取り扱いだった。

そのため、5~6年前から、どこかのタイミングで仕事を辞めてインプットに専念する時期を作りたいと考えるようになった。しかし金銭的な余裕がない、卒業後に雇用され得るだけのスキルがない、という2つの理由で躊躇していた。しかし幸いにして、ここ数年でエンジニアとしてはそこそこのレベルになれたので最低限食うには困らないという気持ちになり、かつ貯金がそこそこ貯まったこともあり、やっていきが高まってきたので実行に移すことにした。

大学院という選択肢について

貯金があると言っても仕事を辞めないにこしたことはないので、当初は働きながら夜間に通える大学でCSを勉強しようかなと思っていた。しかし、日本だと夜間コースでCSが勉強できる大学は殆どなく*2、またカリキュラムを見ると基礎的な内容が多く、今の自分にとっては学びが少ないように思えた。通信教育系のカリキュラムもいくつか調べてみたけど、どれも基礎的な内容というかプログラミングのカリキュラムという感じで、どうにもしっくりこなかった。

なので、思い切って仕事を辞めて普通に大学院に行くことにした。
色々調べた結果、今の日本ではちゃんとしたCSを勉強するには昼間に通う大学院に行くしかないと思えた。その事実の是非はともかく、今自分が新しいことを勉強できる環境に身を置くとしたら、大学院で研究することが最も効率的な選択だと思えた。

入学までの経緯

別のブログに院試までの準備とか最近の様子とかをまとまているので、興味があればこちらを参照してほしい。
今後も学生生活ネタや研究ネタはこちらのブログに書いていく予定である。

serihiro-graduate-school.hatenadiary.jp

大学院で何を研究するのか

配属はHPCS研究室というHPC関連の技術要素を扱う研究室で、もともと興味があったMapReduce on HPCでの性能改善に関するネタを探そうと思っていた。しかし、MapReduceはあまりにレッドオーシャンで論文のネタになりにくそうという話になり、深層学習 on MapReduce(もしくはそれに類する分散環境)はどうかという話となった。

深層学習については以前に一度本を読んで写経した *3ぐらいの知識しかなく、TensorFlowやChainerもMNISTチュートリアルをやったぐらいの経験しかない。しかし、せっかく時間を取って研究するならやったことがない事の方が学びが多かろうということもあるので今は深層学習について論文を読んで知見をinputしている。同じ研究室内には深層学習を扱っている人はいないので何とか一人で頑張ってみる次第である。

最近の様子

大体、朝8時から19時か20時ぐらいまでは大学内にいる感じである。*4

M1の間に必要な単位をできるだけ稼いだ方がよさそうなので、5日間の平日のうち4日は何らかの講義を1日に1,2個履修している。CS専攻のカリキュラムページ はMkDocsで書かれていて読みやすくて良い。
今取っている講義は、自分の研究に関連する分散処理に関するものや、自分が単純に興味がある遺伝的アルゴリズムや数理最適化に関する理論などである。他にも、インテルの中の人が来て最近のインテルのCPUアーキテクチャに関する説明をしてくれる講義なんかも取っている。色々あって楽しい。

講義以外の時間は研究室にいて、今月は研究テーマを決めるための関連研究の調査をしている。まずは基礎的な所からということで、MapReduceや分散深層学習に関連するものを中心に読んでおり、や英語の論文を読んで要約する作業も最初はかなり時間がかかったが、2,3本読み切った辺りから段々と早くなってきた気がする。あと深層学習について復習するために ゼロから作るDeepLearningを最初から写経しながら読み直した。*5

それ以外だと、大学の図書館に行ってみたら大量に技術書があって、何時間でもいられそうな環境であるということを発見したので帰宅する前とかに図書館に寄って漁ってたりする。この土日も図書館に行って技術書を読んでいた。詳解LinuxカーネルとかTypes And Programming Languagesとかドラゴンブックとかの、普通に買うと余裕で7000円とかする本がタダで読めるのは大変ありがたい。大学にいる間にできるだけ読んでおきたい。

お金の話

「無職なのにお金はどうするの?」という話をよく聞かれるのだが、率直に答えると、必要な予算を計算したところ、自分の貯金を全部使っても若干足りなかったので、足りない分は親からの借金でカバーしている。
大学の入学金・授業料*6は国立なので大した額ではないのだが、2年間の住民税*7国民年金・保険*8・生活費が、必要予算の大半を占めている。2年間は旅行はおろか、技術系カンファレンスに日帰りで行くことも厳しくなりそうだが致し方ない。改めて自分が生きていくのに必要な予算を計算することで、ただ生きているだけで物凄くお金がかかるということを実感できる良い機会になった。卒業後は、借りた金を早く返せるように、また貯蓄を増やしなおすべく、前職以上に稼げるようになろうと決意した。

卒業後の話

もちろん就職はするのだが、どの企業を受けるかまでは決めていない。
できれば分散システムを利用したプロダクト開発に関われるソフトウェアエンジニアとしてのポジションに就きたいと思うが、具体的な就職活動的なものは来年以降にやることとし、今年は研究に専念したいと考えている。また、在学中のインターンについても行ってみたい会社がいくつかあるので、その辺りにも挑戦してみようと思う。

*1:独学でやった勉強と言えばパタヘネを読んだぐらいか。

*2:例えば東京電機大学には夜間に学べるコースがあるが

*3:その時の話は別エントリに書いた https://serihiro.hatenablog.com/entry/2016/12/22/000000

*4:前職時代がこんな感じだったのでそれをそのまま引き継いでいる感じ

*5:余談だが、最初の方の版は誤植が多いので、古い版を持っている場合はgithubにあるerrataを見ながら読んだ方がいい

*6:実際の額は http://www.tsukuba.ac.jp/admission/graduate/tuition.html を参照

*7:特にフルタイムで働いていた2017年度の所得で計算される2018年度分がとてもとても痛い

*8:1年目は関東ITSの任意継続の方が安かったのでそちらを使い、2年目からは国民保険の方が安いのでそっちに切り替える

どうやったら成長できますか的な質問にうまく答えるのが苦手だ

これまで散々意識高いポエムをこのブログに書いてきたのに、具体的に「こうすれば成長できるぜ」という答えが、うまく言葉にできないでいる。

大体、自分自身は成長したいと思ったことはない。
ただやりたいことが目の前に転がっていて、それを実現するためにうにゃうにゃ悩みながらやってきたら、結果として「とある時点の自分と比べると成長したな」と言える程度にはなっている、という程度。そもそも成長というものが良く分かっていない。

例えば、勉強すれば成長できるのだろうか。自分の場合、勉強のための勉強が大の苦手だ。
目的が分からず、その上興味も持てない勉強をするのがすさまじく苦手だ。なので勉強についてもうまく説明できない。残念。

基本的に、勉強は必要が生じたらやる、いわゆる「遅延評価勉強スタイル」を採用している。以下のサイトで紹介されているものに近い方法だと思う。大体これでうまく行っている。

d.hatena.ne.jp

以前高校数学を勉強したのも、古典機械学習を勉強しようとしたら数式を見て脳が処理できずにフリーズしてしまったので、スムーズに読むために勉強した程度である。

serihiro.hatenablog.com

もし、勉強というものが成長につながると仮定するならば、勉強の機会を沢山つくればいい、ということになるという仮説が立てられる。
つまり、自分が勉強が必要だと感じる状態を作り続ければいいのではないだろうか。

例えば、月並みだが、やったことのないことをやってみる。そうすれば、知らないことやできないことにたくさん直面するだろうから、勉強の必要性が生じ、勉強することになるだろう。

このブログでは今まではそんな感じの論調でエントリを書いてきたが、こういう話をリアルですると、コメントとして「なにをすればいいかわからない」「やりたいことからやるとしても、やりたいことが見つからない」と言われることが多い。じゃあやらなければ?と言いたいところだが、それだと成長できないというめんどくさいデッドロックになってしまう。

「成長したいがやりたいことはない」という状態は解消するのがとてつもなく難しい。じゃあ成長しなきゃいいじゃん、と回答したくても「成長しないといけない気はしている」という話になってしまうのであまり雑なことも言えない。

こういうことに対する回答としては、以下のようなエントリを自分は過去に書いている。

serihiro.hatenablog.com

serihiro.hatenablog.com

serihiro.hatenablog.com

しかし、どれも結局は「何となくやりたいことがある」という前提の回答になってしまている気がしている。

実際、自分は今、少なくとも30代のうちに達成したい目標があり、それを達成するために1年後、2年後はこうなっていたい、というロードマップがある。*1だから、そのロードマップの途中に設定されたマイルストーンを達成するためには、今後1年はこういうことを勉強しないといけないなぁ、というのも自然と見えてくるので、今時点の状態に関して言えば、自分がすべきことは明確に見えている。

だから、結局は、そうじゃない人のことなんて良く分からない、というのが本音である。
やりたいことがあるから今その環境にいる、というのが自分の中では当然になってしまっている。仮に、今はやりたいことができる環境におらず、やりたいことができる環境が明確に分かっているのなら、環境を変えられるように動く。そういう環境の有無が分からなければ、環境はそのままで自分を変える。それが自分のスタイルで、それ以外のスタイルで生きている人のことは正直わからない。それが自分という人間の限界である。
特に仕事に関してはその傾向が強い。そうでなければ、一番安定していたであろう新卒で入ったSIerに今でもいた気がする。趣味に関しても、かつてはマンドリンオーケストラに所属していたが、それよりもやっぱ独奏をメインでやりたいと思うようになってマンドリンオケ活動を全て辞めて、ギター教室に通うようになったりした。これもある種、やりたいことをやるための環境の変化ともいえる気がする。

こんな調子なので、やりたいことがない、けど成長したい、という人に対してアドバイスできることなんて全くないんだなぁということに最近気づいた。やりたいことがあるがどういうアプローチで攻めればいいか分からないのでブレストの相手になってくれ、というのであれば対応できそうなものだが、どうすればいいですか、と聞かれても「いや私も分からないんですよ」と言うしかない気がしている。

*1:30代というのは大きくピボットするための体力と精神力を維持できるのは30代が限度じゃないかと周りの30代を見ていて思った、という程度の根拠

RubyでMapReduceを実装している

去年の12月ぐらいからRubyMapReduce*1を実装している。
一応、ちゃんと複数のマシンで分散処理ができるところまで実装できたので、今の進捗をまとめておく。

github.com

最初は分散処理で動作するものではなく、1台のマシンでマルチスレッドで動作する疑似分散処理の実装を作ってMapReduceアルゴリズムの理解を深めるのが目的だったが、せっかくなのでちゃんと複数台のマシンで動作するものを作ることにしてみた。

概要

システム全体のアーキテクチャは以下の通り。

f:id:serihiro:20180212175809p:plain

とは言っても、実用が目的ではないので以下の制約がある(Combinerはそのうち実装するかも)

  1. Maptaskは1台のnodeで動作し、Reduectaskは空いているJobWorkerの分だけshuffleして並列に動作する
  2. Combiner未対応なのでMaptaskのoutputをそのままshuffleする
  3. shuffleはkeyのhash値を空きJobWorkerの数で割った余りを使う単純なhashパーティションのみ
  4. MapReduceのinput/output/処理途中の中間データは全てS3に保管し、データのローカリティは一切考慮しない
  5. 途中で処理が失敗してもリカバリする手段はない

ローカルで動作させている様子をキャプチャしてみたが、なんとなくこれで伝わるだろうか。
S3の代わりにminio*2をローカルで実行し、JobTrackerが1host, JobWorkerが3host起動しており、maptaskが1hostで実行され、その後, reduectaskが2hostで実行されている様子である。

www.youtube.com

使い方

クラスタの起動の仕方はREADMEに書いたのでこちらを参考していただきたい。 Dockerイメージも用意したので、めんどくさい人は docker-compose up 一発でクラスタを起動できる。

ジョブは、MapタスクとReduceタスクをそれぞれ別クラスで用意すればよい。 なお、自分で用意するのがめんどくさい人向けにCLIにサンプルが入っており、手順だけまとめると、docker-compose upで起動した場合は以下のようにすればWordCountが動かせる。

$ docker-compose exec job_tracker bundle exec simple_map_reduce generate_lorem_text_data --upload=true
$ docker-compose exec job_tracker bundle exec simple_map_reduce execute_word_count

ちなみにここで実行しているWordCountのJobは以下のようなコードである。*3

class WordCount
  def map(input_data, output_io)
    input_data.split(' ').each do |raw_word|
      word = raw_word.strip
      next if word.empty?
      word.delete!('_=,.[]()#\'"-=~|&%')
      word.downcase!

      output_io.puts({ key: word, value: 1 }.to_json)
    end
  end
end
require 'json'
class WordCount
  def reduce(input_io, output_io)
    output = Hash.new(0)
    count = 0
    input_io.each_line(chomp: true, rs: "\n") do |line|
      input = JSON.parse(line, symbolize_names: true)
      output[input[:key]] += input[:value]
      count += 1
      if count % 100 == 0
        puts "current count: #{count}"
      end
    end

    output.each do |key, value|
      output_io.puts(JSON.generate(Hash[key, value]))
    end
  end
end

これをどうやってJobTrackerに渡して実行しているかというと、ソースコードをStringとしてJobTrackerにPOSTし、temporaryなクラスを生成した上でそのクラス内クラスとしてclass_evalして定義する、という力技により実現されている。*4
Hadoopだとjobをjarとして生成してjarをそのままNameNodeに渡すようになっているが、スクリプト言語である以上こうするしか思いつかなかった。。

使ったライブラリなど

sinatra

実はsinatra*5をちゃんと使ったことがなかったので、勉強を兼ねて使ってみた。
sinatra自体は歴史あるプロダクトなので今さら特に語ることもないのだが、1クラス1アプリの単位で実装できるのが結構都合がよかった。今回作った実装ではデータの永続化を一切しておらず、sinatraアプリとして実装したクラスのクラスインスタンス変数にすべて突っ込む力技*6を採用しているのだが、データストアとしての役割も兼任させる上では1クラス1アプリという単位は管理上都合がよかった。

また、rubyのクラスとしてweb appをそのまま実装して起動できるので、rubyからふつうに起動できるのも便利だと思った。railsだと bundle exec rails s みたいな感じで、普通はシェルスクリプトなどから実行するしか手段がないのだが、sinatrarubyスクリプトとして実行する以外の起動手段を持っている。

例えば今回作ったMapReduce実装には管理用のCLIを添付したのだが、このCLIでJobTrackerを起動する部分は以下のように実装している。*7

SimpleMapReduce::Server::JobWorker.run!(port: config.server_port, bind: '0.0.0.0') do
  SimpleMapReduce::Server::JobWorker.setup_worker
end

run! で起動できるのは割と周知された方法だと思うが、さらにblockを渡すことで起動前に処理を独自のcallbackを追加できる。これはドキュメントを調べても見当たらず、結局sinatraのソースを眺めていて見つけた。*8

また、同様にソースを見ていて発見した例として、sinatraアプリを終了するときに実行されるSinatra::Base.quit! *9をOverrideすることで、sinatraアプリを終了する時にもcallbackを挟むことができるので、WebRickを終了させる前にWorkerを終了させるのに利用している。*10

# @override
def quit!
  @keep_polling_workers = false
  @polling_workers_thread.kill
  job_manager.shutdown_workers!
  super
end

MessagePack

Web APIへデータを渡すときのserializeはいつものようにJSONでいいかなとも思ったが、せっかくなのでMessagePack*11を使ってみた。
独自にTypeを定義すれば自前のクラスもserialize/deserializeできるようだが*12、今回はとりあえずHashとして各種プロパティの値をdumpしたデータをserializeしているだけである。いずれ直接JobやTaskをserialize/deserializeできるようにしてみるのも面白いかもしれない。

Worker Threadの管理

Threadで非同期実行するWorkerを管理をするために別のgemを実装した。

github.com

中身を見てもらえば分かるがSidekiq*13っぽいI/Fで指定したクラスをjobとしてキューイングして、Thread poolに入ってるThread群で並列に実行できる、という程度のものである。JavaのConcurrency UtilにあるExecutorServiceみたいなものの超簡易版ぐらいに思ってもらえれば幸いである。

Sidekiq等のJobQueueと異なる点として、外部のデータストレージをキューストアとして使っていない。漢らしいオンメモリキューストアである。もちろんjobの優先度も管理していない。 jobをEnqueueする部分はこんな感じである。*14

module Rasteira
  module EmbedWorker
    # Manager class that manages the thread pool and executes jobs.
    class Manager
      attr_reader :job_pool

      def initialize
        @job_pool = []
        @thread_pool = []
        @mutex = Mutex.new
      end

      # 省略

      def enqueue_job!(worker_name, options = {})
        @mutex.synchronize do
          @job_pool << ::Rasteira::Core::Job.new(worker_name, options)
        end
      end

データストアが絡まないとここまで実装をシンプルにできるんだなぁと感心した。逆に、普段どれだけ外部システムとの連携に神経をすり減らしているかが良く分かる。

この漢らしい実装のおかげで、JobTrackerやJobWorkerのsinatraアプリとメモリ空間を共有することができるため、jobやworkerなどのオブジェクトの参照をそのまま渡すことができる。
なので、workerからjobやworkerなどのオブジェクトを直接変更することができる。セキュリティもへったくれもないのだが、実用を考えていないのでセキュリティには一旦目をつむりたい。*15

感想

MapReduceそのものの実装は簡単だったのが、その周辺のworker, job, taskを分散環境で管理するのが難しく、分散処理に関する実装に殆どの工数が取られてしまった。
分散処理で動作するプロダクトを作る際に本質的でない実装にものすごく工数がかかるのはGoogleの論文にも書いてあった通りで、まさに身をもって再試をした気分である。

しかし、お陰で何となくHadoopなどの分散処理フレームワークが裏で何をやってくれていて、その恩恵によってどれだけ本質的な処理にだけ集中できるようになったのかが少しは理解できたと思う。 今回作った実装についてはまだいろいろと実験しがいがありそうなので、今後も時間を見てアップデートしていく予定である。 今は分散処理や大規模データ処理の高速化のアルゴリズムについて興味があるので、今後も実装を通じて学んでいきたい所存。