フジーコの日記

自分が試してみたプログラミング関連のブログです

ドイツのスタートアップでコロナワクチン証明アプリの開発に関わった

色々あって今年の1月からドイツのスタートアップで働き始めた。メインのビジネスドメインはIoT、ブロックチェーン周りなのだが縁あってコロナのワクチン証明アプリの開発に関わることになったのでその話と入社からの7ヶ月を簡単に振り返る。

振り返り

1月
  • 入社。完全フルリモート環境
  • 自分以外皆ほぼドイツ語ネイティブ。自分がいるミーティングでは英語で話してもらう
2月
  • 新しい環境、ロックダウン等などでストレスがピークに
3月
  • 割と労働環境に慣れてくる。
  • ここまでは既存システムの開発をしてきたが、ここでコロナプロジェクトが動き始める
4月
  • コロナプロジェクト本格始動
  • 徐々に忙しくなってくる。
5月
  • 忙しさピーク。休日も休みほぼなし
  • 人生で一番コード書いた1ヶ月だった
6月
  • 引き続き忙しい。
  • 遂にリリース完了

コロナプロジェクトについて

ワクチン証明アプリはこのCovPass Appというアプリで2回目のワクチン接種後にもらえるQRコードを読み込めばそのQRコードが有効かどうか証明してくれるというもの。自分達の会社はこのアプリの開発を担当したのではなくQRコードを発行する側のシステム(ワクチンセンター向け)を開発を担当した。ドイツではワクチン2回接種して2週間経ったか、24時間以内のコロナテストの陰性証明がないとレストランの中で食事したりジムに行くことができないので、このアプリはそのワクチン接種の有効性を簡単に証明するために使われることになる。このプロジェクトはIBMなどの大企業が関わった国レベルのプロジェクトなのだが、そのプロジェクトに50人ほどのスタートアップがどうやって参加できたのかは正直あんまり知らない。そこまで話を持っていった経営陣とビジネスサイドが凄いとしか言えない。

f:id:fuzyco:20210731064329j:plain
コロナワクチン証明アプリ

ubirch.de


ここでは実装の詳細は話さないが、実装のポイントは偽のQRコードが作ることができないようなセキュアなシステムを開発することだった。

実装よりもチャレンジングだったのはスケジュール。ドイツが本格的にワクチン接種を開始する(6月中旬くらい)までに、ワクチンセンターでQRコードが発行できる状態にする必要があった。4月頃から本格的に開発が始まったが、その段階でも完全に要件が決まっているわけではなかったので締切りは決まっているがスケジュールを引くのが難しい状態。
普段は2週間スクラム開発をしているが、このプロジェクトに関しては全く機能しなかった。週あるいは日単位でタスクが降ってきて、有識者を捕まえて要求の理解、仕様決め、設計、実装をやるというサイクル。あとGDPRはやはり考慮する必要があり、本社がヨーロッパ外のクラウドサービスは基本使えない。つまり、AWSGCP、Azureのサーバーは使えないという制約があった。上記にも書いたとおり、5月頃に忙しさがピークになった。3月まではほぼ残業なしだったのから一転して休日出勤や深夜残業は当たり前になった。他の同僚もかなりストレスが溜まっていて、自分がミーティングにいても関係なくドイツ語で話すみたいな状況が多々あった。

正直自分のエンジニア人生の中で最も忙しい2、3ヶ月だったが、とても良い経験だった。無事リリースに間に合わせることができたし、リリースしてから何千というQRコードが発行され、毎日使われているのを見るのは達成感があった。実際自分も2回目のワクチンを接種した後に、自分が実装に関わったQRコードが目の前で発行されるのはシンプルに嬉しかった。あと開発したシステムが動く瞬間は何にも代え難いものがある。メインの2つのマイクロサービスを主に開発していたがその二つのコンテキストが重なり合って一つのユーザー体験に繋がった瞬間はかなり感動した。がっつりScalaを書くことができたし、普段のシステム設計、実装は勿論だが、それに加えて楕円曲線暗号、AES、電子署名、mTLS、X509証明書、COSEオブジェクト等などセキュリティ面で色々と勉強になった。また、かなり残業したがその分のお金がしっかり支払われることになった。

労働環境について

最後にドイツの労働環境について軽く触れておく。特に労働時間と休日の考え方が日本とは大きく異なる印象。給料と同じくらい休暇が大事な印象で、長く働いて給料を多くもらうくらいなら休日を多く取る方を選択する人がかなり多いと思う。実際週3, 4日の契約にしている社員もいる。自分は年に有休が30日あるのだが、まだ自分は日本の労働感覚を持っていたので、最初はどうやって使いきればいいのかわからなかった。同僚に「皆どんな感じで休暇とるの?」と聞くと、「え、いや自分のとりたいタイミングでとりなよ。Hirokiは30日有休持ってるから合計6週間分休暇取らないとダメだよ」と言われた。正直日本の会社で働いたときは、有休使えたら使おうとくらいの感覚だったが、こっちだと有休は全部使わないといけないという感覚である。自分も8月に1週間、9月中旬から4週間休みを取る予定だが、同僚にも上司にも特に取りすぎだとは言われなかった。もちろん引き継ぎはしっかりやる必要がある。また、病欠は病欠で有休とは別。なので同僚が2週間の休暇終えるときに、「先週は体調崩してたから来週その分休暇にする」と言ってきたときには、一瞬え?と思ったが、ああ確かにシステム的にはそれで問題ないなと納得した。

2018年振り返り

2018年を社内・社外での発表資料と共に振り返る。

1月「Try Cats」

会社で行われた新年大勉強会において、年末年始に触ってみたcatsについて発表した。と言っても、catsのEvalモナドの説明とそもそもモナドは何なのかに焦点を当てた資料で、あまりcatsについて触れなかった。触れなかったというよりも、サンプルコード触ったくらいだったので話すこと自体がこの時点ではあまりなかったという表現の方が正しいかもしれない。

fuzyco.hatenablog.com

上の記事において、コメントでMonadErrorはScalazにもあると指摘された。そこでMonadErrorに興味を持ち、調べてまた別の機会に会社で発表したのが以下の資料。

speakerdeck.com

4月「Extensible Effects」

3月に開催された、Scala Matsuri 2018で聞いた発表を咀嚼して社内で発表した。自分は@halcat0x15aさんのExtensible Effects in Dottyの発表を会社で共有した。会場で聞いた時は、内容が難しくて全く理解できなかったので、自分なりに色々調べたりコードを書くことによって理解して共有した。発表資料は、dottyよりもextensible effectsの概要とScalaでの実装がメイン。

speakerdeck.com

4月 bq_sushi#7

会社にbq_sushiの運営をやっている人がいたので、その人に推薦してもらって、#bq_sushi tokyo #7 - connpassで発表する機会を頂いた。内容は、業務で作っていたGAEを用いたバッチシステムについて。GAEとTaskQueueを用いて、BigQueryの制約を回避しながら、どのようにデータを挿入していくかに焦点を当てた発表。発表資料の最後に、「Dataflowを使えばこんな頑張らなくてもいいかも」といった旨の内容を書いたが、正直このバッチを作る時にDataflowの存在を知らなかった。技術選定の際の調査は大事だなと痛感した。

また、この発表資料作成はかなり苦戦した。最初に作った時は、実際に使っている広告システムの用語を至る所に使っていたので、発表練習の時に内容が難しすぎると指摘をもらった。余計な部分を抽象化して、話したい所だけを具体的にして聴衆の注意を分散させないことを資料を作る上でより重要視するようになった。先輩から、「俺の発表はこれだけわかりやすくしても伝わらないんだ」と思いながら資料を作ると良いよというアドバイスをもらったことを鮮明に覚えている。

speakerdeck.com

7月 「Scalaでのマルチスレッド処理について」

11月のScala関西サミット登壇に向けて、この時期くらいから並行処理をテーマに何か話したいなと思っていた。その準備として、社内で以下の資料を発表した。volatile変数とかsynchronizedなどの現代ではあまり使わないようなJavaの並行処理の基礎的な内容がメイン。自分自身ここら辺の基礎的な内容ですらあまり知らなかったので、良い知識の整理になった。

speakerdeck.com

11月 Scala関西サミット

運良く、並行処理をテーマにしてScala関西サミットにCFPを提出したら通ったので、発表してきた。人生で初めての45分枠の発表かつまだ自分自身全然知識が浅い並行処理がテーマの内容での発表。正直自分を追い込み過ぎた感があり、準備はかなり大変だった。
ただ準備の段階であらゆる並行処理の書籍を読んでサンプルプログラムもかなり書いたので、準備過程で得たものはかなりあった。発表を成功させることも大事だけど、準備過程で「こうゆう質問が来るかもしれない」とか「あれ?じゃあこの場合はどのような挙動をするんだろう」といった資料内容+αの知識の準備で得たものが大きな財産になるんだなと痛感した。

4月のbq_sushiでの資料作成と同様に、資料の構成もかなり苦戦した。この時は、当初自分が講義っぽい資料(いわゆる先生が生徒にするプレゼン)を作っていたのだが、それを先輩に指摘され聴衆と目線を合わせた資料(自分がこうゆう点でつまづいたので、皆さんにも共有したいと思いますみたいな)に変えることによってとてもプレゼンしやすくなったのは新たな発見だった。聴衆の気持ちになりきって資料を作るという観点を得た。

fuzyco.hatenablog.com

総括

振り返ってみると、今年はプレゼンの資料作成の過程で知識を得るということが多かったなと思う。自分で勉強して(インプット)、それを誰かに説明すること(アウトプット)のサイクルの大切さを体感した一年間だった。

発表内容としては、bq_sushi以外はScalaが多かった印象。GAE、TaskQueue、BigQueryなどのGCPモジュールは業務で触り続けているが、個人の勉強はかなりScalaに比重を置いていたので、当然と言えば当然の結果。1月時点で、catsの型クラスって何だっけ?とか並行と並列の違いって何なんだ?と疑問に思っていた当時に比べればかなり知識がついたなと思う。

来年の目標

最近Cats Effectなどのcats系のライブラリを触っているので、触る過程で関数型プログラミングの特徴・利点を自分の言葉で話せるように整理及びプロダクトへの導入をやりたい。また、OSSのコントリビュートを目指して行きたいと思う。

Scala関西サミット2018で発表してきた #scala_ks

Scala関西サミット2018で、「Scalaでの並行・並列処理戦略」というタイトルで発表してきた。

2018.scala-kansai.org

発表資料はこちら。
speakerdeck.com

感想

結構煽ったタイトルでCFPを出したので、資料の作成にかなり苦しんだが、逆に作る過程で色々勉強して自分の中で整理できたので結果的に良かった。
懇親会などでScalaの著名な方々とお話しする機会があったので刺激をもらえた。

反省

初心者向けのセッションとして応募したが、そもそも初心者とは具体的にどうゆう人を指すのか。自分の発表のテーマに限って言うと、
1. Scalaちょっとは書いたことあるけど(Optionとかfor式とか)Futureなどの並行/並列系のオブジェクトは触ったことない
2. FutureとかAkka Actor触ったことあるけどExecutionContextとか特徴とかまでは掘ったことがない
の二つがまず考えられる。自分の発表に限っては後者の2向けっぽい感じになったけど、1の人にとっては情報量が多すぎてなんかよくわからないまま終わってしまったという感覚に陥る可能性が大いに有りえた。自分自身ハードルをあげてしまうのにビビって初心者向けとして応募したが、今後ちょっと踏み込んだ内容を話せるなと思った時は、恐れずに中/上級者向けで応募して、マサカリを恐れずに知見の共有を進んでしていこうと思う。

あと、タイトルに戦略という文字を入れたこともによって聴衆の期待を煽ってしまった。現に発表を開始して2人ほど部屋を出ていってしまった。おそらくもっと高度な内容(パフォーマンスチューニング、ライブラリの深い構造など)を期待していた中・上級の方々だったのであろう。

細かい話でいくと、資料のサンプルコードが小さすぎた。会社で発表練習した時はかなり大きいスクリーンを使っていたので気にならなかったが会場のスクリーンだとかなり見にくかった。次からは短いコードを大きい文字で貼れるような発表構成にしようと思う。

今後

並行処理を調べる過程で出てきた低レイヤ周りの知識の整理(アクターの軽量プロセスって結局何なのか、スレッドとは違うのかなど)がちゃんとできていないのでその辺を整理して、どこかでアウトプットしようと思う。

終わりに

このような貴重な機会を設けてくださったScala関西サミットの運営の方々、発表を聞きに来てくださった方々、本当にありがとうございました。

Scalaの関数型ライブラリCats触ってみた

年末年始にScalaの関数型ライブラリCats触ってみたので、Catsについて社内の大新年勉強会で発表した。

参考文献

猫番 — 猫番

Scala with Cats - Underscore

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

所感

  • category(圏)が名前の由来になっているだけあって、参考文献には圏論に沿った説明が記述されていた。理解するのが難しかった。ただ、その後改めてカラーコップ本(Scalazの本)を読むと、PARTIIIの10章「モノイド」、11章「モナド」、12章「アプリカティブファンクとトラバーサブルファンクタ」の章の理解が深まった。
  • CatsのEvalモナドが個人的には興味深かった。特にEvalモナドによるスタックセーフな遅延演算をサポートにより、スタックオーバーフローを回避できるといった例の所。今まで再帰処理では継続渡しスタイルを用いれば末尾再帰最適化されてStackOverflow回避できると思っていた。でも、それでは解決できないケースがあるから、トランポリン化を使って回避しようねってゆう話なんだけど、これもカラーポップ本の13章で出てきてて、初めてこの章読んだ時に全然理解できなくてさらっと呼び飛ばしたの思い出した。改めて読み直してみると、凄い理解が深まった。
  • 全体通して、Catsを勉強したことによって、今まで関数型プログラミングの書籍読んで全然概念やメリットが理解できなかった所の理解が進んだ。今後、Catsを業務やプライベートでScalaで何か作る時に使っていきたい。

Scalaの行列計算ライブラリBreezeによるMatrix Factorizationの実装

Matrix FactorizationをScalaの行列計算ライブラリであるBreezeで実装してみました。

Matrix Factorizationとは、推薦アルゴリズムの手法の一つである協調フィルタリングで有名なモデルです。 ユーザーの推薦対象物(以降アイテムと呼ぶ)への評価のデータを元にユーザーとアイテムの潜在ベクトルを学習するモデルです。 Matrix Factorizationの説明は以下のリンクに詳しく書かれています。 qiita.com

本記事ではこのMatrix FactorizationをScalaで実装して、Pythonによる実装と処理速度を比較してみました。

Python機械学習においてよく使われる言語です。 Pythonは行列計算を行うライブラリが多く有り、そのライブラリの中身はcで書かれているため、動的型付け言語でありながら、ライブラリを上手く使うことによって、簡単に行列計算を高速に行うことができます。

一方でpythonは動的型付け言語のため、以下のようなデメリットが生じます。
1. 型エラーなどの事前条件エラーを防げない。
2. 変数や関数の引数、返り値の型がわからない。

また、PythonにはGIL(グローバルインタプリタロック)の仕組みがあるため、容易にマルチスレッドで並列に処理を行うことができません。

一方で、Scalaは静的型付け言語であるため、上で述べたPythonのデメリットに対して、以下の点で解決できます。
1. コンパイラによる事前条件エラーのチェック
2. 型によるコードの表現
3. 標準で搭載されている並列コレクションによる容易な並列処理の実現

前置きはこれくらいにして、早速比較をしていきます。
本記事ではMatrix Factorizationはユーザーベクトル、アイテムベクトルによるシンプルな回帰モデルであり、目的関数には正規化を用いたものを実装します。

回帰モデル   { \displaystyle
\begin{equation}
\hat{r}_{u,i} = \vec{p_u}^T\vec{q_i}
\end{equation}
}

目的関数 { \displaystyle
\begin{equation}
 {min\_{P, Q} \sum_{(u,v) \in R} (r\_{u,i}-\vec{p_u}^T\vec{q_i})^2 + \frac{\lambda}{2} (\parallel \vec{p_u} \parallel _{F}^2 + \parallel \vec{q_i} \parallel _{F}^2) }
\end{equation}
}

まずはPythonによる実装ですが、以下のリンクを参考に実装を行いました。

上のリンクの実装に少し工夫を加えて、ループ処理を行う学習部分を以下のようにCythonで実装しました。 全コードは以下のgithubを参考にしてください。

github.com

cdef class FastMF(object):
  
   ...

   """
   CythonによるMatrix Factorizationの学習
   主にforによるループ処理の高速化を目的とする
   """
   def learning(self):

        cdef:
            double err = 0.0
            int step
            long user_index
            long item_index
            double start
            int k
            double all_error = 0.0
            np.ndarray[DOUBLE_t, ndim=1, mode = 'c'] user_matrix
            double rating

        for step in xrange(self.steps):
            for user_index, user_matrix in enumerate(self.R):
                for item_index, rating in enumerate(user_matrix):
                    if not rating:
                        continue
                    pre_u = np.transpose(self.P)[user_index]
                    err = self._get_rating_error(user_index, item_index)
                    np.transpose(self.P)[user_index] += self.gamma * (2 * err * np.transpose(self.Q)[item_index] - self.beta * np.transpose(self.P)[user_index]) # ユーザーベクトルの更新
                    np.transpose(self.Q)[item_index] += self.gamma * (2 * err * pre_u - self.beta * np.transpose(self.Q)[item_index])  # アイテムベクトルの更新
           
            all_error = self._get_error()
            print all_error
            if all_error < self.threshold:
                self.nR = np.dot(np.transpose(self.P), self.Q) # 得られた評価値行列
                return
        
        self.nR = np.dot(np.transpose(self.P), self.Q)

次にScalaによる実装を行いました。
この実装では、BreezeというScalaの行列計算ライブラリを用いました。
BreezeはPythonのNumpyのように配列の初期化、行列同士の足し算、内積などの行列計算を容易に行えるライブラリです。

全コードは以下のgithubを参考にしてください。 github.com

Breezeを使うことによって、行列計算も簡単にできました。さらに、Scalaの標準で用意されているコレクションメソッドであるparを用いることによって簡単に並列に学習を行うようにできました。 また、Scalaのcase classを用いることによって、Pythonよりも型による表現力が上がりました(あくまで個人的な意見ですが、、)

/**
  * Matrix Factorizationモデル 
  */
class MatrixFactorization(useIdMap: UserIdMap, itemIdMap: ItemIdMap, K: Int) {

  // ユーザー重み行列
  val userW: MFW = ...
  // アイテム重み行列
  val itemW: MFW = ...
  ...

   /**
    * 学習
    */
  def fitIterator(mfd: MFD, epochs: Int = 30, eta: Double = 0.005, lambda: Double = 0.02, threshold: Double = 0.1): Unit = {
    // parによる並列で処理を行う
    (1 to epochs).par.foreach { i =>
      fit(mfd.iterator, eta, lambda)
      val allError = getAllError(mfd, lambda)
      println(allError)
    }
  }

  @annotation.tailrec
  private def fit(mfdIter: MFDIter, eta: Double, lambda: Double): Unit = {
    if(mfdIter.hasNext) {
      val (userId, itemId, rate) = mfdIter.next
      if(rate != 0) {
        val error = rate - predict(userId, itemId)
        for(k <- 0 until K) {
          val prevU = userW.value(userId, k)
          userW.value(userId, k) += eta * (2 * error * itemW.value(k, itemId) - lambda * userW.value(userId, k))
          itemW.value(k, itemId) += eta * (2 * error * prevU - lambda * itemW.value(k, itemId))
        }
      }
    fit(mfdIter, eta, lambda)
    }
  }
}

// 特徴ベクトルのケースクラス
case class MFD(value: DenseMatrix[Double]) extends CFD[Double] {
  def iterator = MFDIter(value.iterator)
}

// DenseMatrix[Double].iteratorをラップしたケースクラス
case class MFDIter(value: Iterator[((Int, Int), Double)]) extends CFDIterator[Double] {
  def next = {
    val data = value.next
    val userId= data._1._1
    val itemId = data._1._2
    val rate = data._2
    (userId, itemId, rate)
  }
}

次に、Pythonで実装されたプログラムとScalaで実装されたプログラムを実行して、評価を行ってみます。

今回はwebで公開されているMovieLens 100k Datasetのデータを使います。このデータは、各ユーザーが各映画に対して1~5の5段階で評価値を付けたデータです。このうちの80000個の評価データを含むu1.baseを学習に用い、20000個の評価データを含むu1.testをテスト用のデータに用います。評価指標はRMSE(2乗平均平方根誤差)を使いました。 以下にエポック数を100にして行った場合の、学習時間とrmseを示します。実行環境は、CPU: 2.6 GHz Intel Core i5のメモリ: 8GBのMacBookで実行しました。

Pythonによる実装よりも、Scalaによる実装の方が、学習時間が7倍近く早く、精度も遜色ない結果になりました。

学習時間(s) RMSE
Python 216.453 1.177
Scala 31.996 1.045

以上のように、Scalaを用いてMatrix Factorizationを実装した場合、Pythonに比べても行列計算の実装が難しくなく、簡単に並列処理による高速化ができました