Monoxer Intern Report #4_API設計と追加(yokozeki)

自己紹介

こんにちは、モノグサでソフトウェアエンジニアのインターンをさせていただいた横関です。 お茶の水女子大学の4年生で、プログラミング言語の基礎理論を扱っている研究室に所属しています。

参加した理由

AtCoderJobsというサイトで夏季インターンを探していたところ、モノグサのインターンに興味を持ち、応募しました。興味を持ったきっかけは、記憶にまつわる課題をソフトウェアの力で解決しているところが創造的で素敵な会社だと思ったことでした。また、期間が長く勉強になることが多そうだったことも魅力的でした。 参加の最終的な決め手は、面接の時の雰囲気が終始穏やかだったことです。

取り組んだこと

今回のインターンでは、Monoxer APIという外部APIにいくつかのAPIを追加しました。インターンの前半はアカウント管理のためのAPIの追加をしていました。後半は主テーマとして、振り分けルールという便利な機能のAPIを新しく作っていました。

Monoxer APIとは
Monoxerはただの学習アプリではなく、塾・学校・会社など、記憶させたいものがある組織に向けて記憶のプラットホームを提供しています。学習者に何を記憶させたいかは、学習者の属性(所属クラスや志望校など)によって変わります。そのためMonoxerには、学籍番号などのID、〇年〇組といった所属など、色々な情報を登録できます。 クラス変更や学習者の追加などが発生した場合、その情報をMonoxerに反映したいときがあります。その反映は管理画面から行うことが出来るのですが、組織の規模が大きい場合は、管理画面からではなく、その組織のシステムから直接行うと便利です。

Monoxer APIは、そのような場面で使われていて、Monoxerサーバーと外部システムとの間のインターフェースとなっています。 f:id:corp_monoxer:20211007120035p:plain

振り分けルールAPIの追加
Monoxerにはクラスという単位があり、クラスごとに学習者に記憶させたいものを指定できます。振り分けルールとは、アカウントをクラスに自動で振り分けたいときに適用されるルールです。アカウントの情報とクラスの情報を組み合わせて、あらゆるパターンでルールを作ることができ、組織の仕組みに柔軟に対応できるようになっています。

インターン前半で取り組んだアカウント管理APIの方はすでにいくつかのAPIが存在していたのですが、後半の振り分けルールAPIの方は既存のAPIがありませんでした。そのため、JSONの構造や、「どのようなAPIを揃えて振り分けルールの管理を可能にするか」という点から考えました。

API設計
どのようなAPIを揃えるかという部分では、分かりやすく使いやすいAPIにするためにきれいな設計を心がける必要がありました。 Monoxer APIは、RESTの原則に基づいてAPI設計されています。リソース指向アーキテクチャについて勉強してみると、統一的で分かりやすく、おもしろいと思いました。

f:id:corp_monoxer:20211007120131p:plain

得た学び

実際の製品のコードを読んだり書いたりする体験ができて、今まで勉強できていなかった部分(読みやすいコードを書く・APIの設計・データベース周辺)が勉強できました。レビューがとても機能していて、社員の方から、可読性に関するレビューをたくさんいただきました。 Scalaという言語を初めて使ったのですが、Scalaについても色々なことが分かるようになりました。

社員さんとのやりとり

初日にメンターさんから1:1の頻度はどのくらいが良いか聞かれ、小まめに見てもらえるとありがたいと答えたところ、毎日1:1を設定していただけることになりました。心強かったです。
レビューやミーティングではどの社員さんも言葉遣いが柔らかく、働きやすかったです。 またコロナ禍で控えめだったのですが、ボードゲームや、社員の方々・他のインターン生との会話も楽しかったです。

さいごに

エンジニアとして働いたことが無かったので、インターンには勉強になることがたくさんありました。モノグサのみなさま、2ヶ月間ありがとうございました。

Monoxer Intern Report #3_手書き文字評価の改善(chikai)

自己紹介

モノグサ社にてソフトウェアエンジニアインターンとして参加した伊藤 誓です。 現在は東京大学大学院に所属しており、VR環境におけるユーザーインターフェースについての研究を行なっています。

なぜモノグサのインターンに参加したのか

夏期インターンを探している最中に、AtCoderJobsというサイトに乗っている掲載を見て応募しました。参加の決め手になったのは、自社開発を行なっている会社で、幅広い技術領域を取り扱っており新しい技術経験が出来ると感じたことや、「記憶」に対して科学的観点からアプローチしていくというアプリのコンセプトが興味を引いたことなどでした。

取り組んだこと

今回のインターンでは、手書き文字評価周りの改善に取り組みました。その中でも、大きく分けると3つのことに取り組みました。

アルファベット対応
元々、アルファベットの手書き文字評価を行うのには以下のような課題がありました。

  • 十字線の上に表示されていて不自然
  • フォントの形が不自然
  • 1画ずつ分けて書く必要がある

内部で実装されている言語IDやテキストの内容から「アルファベットの問題である」と判断された場合には、十字線ではなく四本線の表示を行いました。

フォントの形に関しては、デザイナーさんとも打ち合わせしつつ、svgファイルの変更を行いました。また、背景に表示する4本線と合うように、svgファイルを編集する処理を行いました。52個もあるファイルに対して編集をするのは面倒なので、ファイルの情報を自動で編集するスクリプトを記述しました。

f:id:corp_monoxer:20211007114724p:plain

繋げ書きに関しては、内部のアルゴリズムの根幹に関わる部分なので、まず既存のライブラリの中身を理解する必要がありました。その後、アルファベットにも対応できるように、「複数回の繋げ書き」「離れたストロークを補間する処理」などを実装しました。

アルゴリズムのテストツールの開発
2つ目に、アルゴリズムのテストツールの開発に着手しました。手書き文字評価では、正解の文字と、ユーザの書いた文字を照らし合わせて正解/不正解を判定しますが、評価の精度が低いとユーザ体験の質の低下に繋がってしまいます。そのため、出来る限り精度を上げる必要があります。しかし、アプリに直接組み込まれている状態でアルゴリズムの精度を評価するのは非常に大変です。そこで、一連の評価/デバッグの流れを簡単にするためのテストツールを開発しました。

アルゴリズムに変更を加えた後、コマンドを1つ叩けば、データベースから作成したデータセットに対してアルゴリズムが実行され、変更による寄与の大きいケースをまとめて出力してくれます。さらに、実行時間やスコアの構成要素などのログも閲覧可能にすることで、より詳細なデバッグが可能になっています。

f:id:corp_monoxer:20211007114830p:plain f:id:corp_monoxer:20211007114840p:plain

アルゴリズムの改善
3つ目に取り組んだこととしては、手書き評価アルゴリズムの改善です。自分で開発したテストツールを用いて、上手くいかないケースを探しつつ、改善に取り組んでいきました。

自分は競技プログラミングを趣味の一つとしていますが、感覚としてはヒューリスティックコンテストで出題される問題に非常に近かったです。一方で、普段のコンテストと違っていると感じた点として、入力ケースのほとんどが暗黙の制約を満たしている場合があったり、最悪計算量が悪いアルゴリズムでもほとんどの入力ケースでは上手くいくため事実上問題ない場合などがあったりして、非常に面白かったです。

学び

自分にとっては、他の人と共同開発しながらプログラムを書くという経験があまりなかったので、その部分は非常に勉強になりました。Pull Requestの出し方や、コードレビューへの応答など、円滑なコミュニケーションを取る上では気をつけるべきことがたくさんありました。

また、自分のコードが本番のアプリケーションに統合される経験も新鮮で、「どのようにして出来るだけ安全なコードを書くか?」といったことを考えるのも新しい経験でした。今までは、強い型付けのある言語のメリットが実感できていませんでしたが、実行時エラーの許されない環境で Kotlin を書いていると、型推論をしてくれる有り難さが身に染みた気がします。

社員さんとの関係

コロナ禍でのインターンでしたが、出社して働くかリモートワークするかが比較的自由に選べる環境でした。私は、週に2回出社して残りはリモートという感じでした。リモートの場合でも、1日に1回はメンターさんとの 1on1 ミーティングが用意されたので、特に困るといったことはありませんでした。

相談したいことがあった場合でも、基本的にメンターさんに Slack を送れば即座に反応してくれるので、助かりました。他にも、Jira などを用いてプロジェクトが管理されており、他部署の社員の方も含めて柔軟な議論をすることができました。社員全員がプロダクトの品質向上に対して真摯に向き合っている様子が伝わってくるので、働いているだけで創造性が刺激される素敵な環境でした。

Monoxer Intern Report #2_誤答生成の改善(egashira)

はじめに

モノグサでソフトウエアエンジニアのインターンに参加した江頭叙那です。現在は慶應義塾大学の環境情報学部3年に在籍していて、VR空間での運動が実空間での動作に与える影響について研究しています。

参加を決めた理由

私は学生時代から学習アプリなどを用いて、いかに楽に暗記できるかを工夫してきた経験があり、モノグサの記憶を手助けするサービスはとても興味があるものでした。また、アプリからフロントエンド、バックエンドまで幅広く開発を経験できることが魅力的でした。

モノグサでのインターンで主に取り組んだこと

私は今回のインターンで主に誤答生成の改善に取り組みました。 モノグサではBook内(学習する問題群)から誤答を生成する機能と、Book外から誤答を生成する機能があり、私はBook外から生成される誤答の改善に取り組みました。Book外から生成される誤答は、正解の単語と文字類似度が近い物が候補として選ばれるような仕組みとなっており、以下のような課題がありました。

  1. 品詞が一致していない場合がある
  2. 誤答の難易度が、正解と大きく異なる場合がある
  3. 十分な誤答候補数を得られなかった場合に、ランダムで関係のない単語が選ばれる

f:id:corp_monoxer:20211007112552p:plain

上記の課題によって、学習者が選択肢の中から消去法で正解を選べてしまうという問題がありました。

誤答の改善方法 

単語に頻度や品詞が紐づいたデータが必要だったため、単語と頻度が紐づいたデータを入手し、それらに品詞の情報を付与しました。品詞情報の付与にはStanza*1 を使用しました。 そして頻度や品詞別に単語をグループ分けし、グループ分けされた単語の中から、正解の単語と近い文字類似度を持つものを誤答として選ばれるようにしました。また、頻度や品詞別で取得した誤答の数が足りなかった場合には既存の手法で誤答を取得し、それらの単語が正解の品詞と頻度が近いほど選ばれやすいようにする改善を行いました。
具体的には誤答生成の流れを以下のように改善しました。

f:id:corp_monoxer:20211007112715p:plain f:id:corp_monoxer:20211007112734p:plain

誤答生成の流れを上記のように改善したことによって、品詞や頻度が異なる単語が誤答として選ばれにくくなりました。さらに新しいデータで単語を取得するステップを追加したことで、候補数が足りずに完全ランダムで誤答が追加されるケースも大幅に減りました。下記の改善例では正解と誤答の品詞が統一されており、消去法で選ばれにくくなったと思います。

f:id:corp_monoxer:20211007112803p:plain

得た学び

チーム開発は少し経験があったのですが、しっかりと会社でレビューを受けて開発を行うプロセスは初めてだったため、多くの学びがありました。まず入社して印象的だったのは、コードにコメントがほとんど書かれていない点でした。初めのうちはコードを読むのに苦労しましたが、開発のガイドラインに書かれていた「コメントがあっても、わかりにくいCodeを正当化することはできません。」という一文に非常に納得しました。おかげで変数名や可読性を意識してコードを書くようになりました。また、インターンで取り組むテーマの実装なども自分で考えることができたため、自分の実装計画を他の人に伝えるプロセスも大変勉強になりました。  

社員やメンターさんとの交流

メンターさんとは週に1~2回ほど1on1ミーティングがあったため、テーマの実装などの相談をしたり、業務以外のことも話すことができました。私は週3日ほど出社していたので、出社時に直接お話できたのがよかったです。他の社員さんやインターン生とはおやつ会などで話す機会があり、エンジニア以外の業務の話なども聞くことができました。また、CEOの竹内さんがたまに話しかけてくれたのが印象に残っています。インターン生である私のテーマや進捗も把握されていて、驚いたと同時に嬉しかったです。

インターンを通じての感想

私はインターン前まではReactを軽く触ったことがある程度の開発経験だったため、インターンでアプリ(iOS Android)、フロントエンド、バックエンドまで触ることができたのはとても貴重な経験でした。また、私が今回取り組んだ誤答改善のテーマは、既存の誤答生成の方法を大きく変えるものであったため、責任感を持ちながら開発に取り組むことができたのも非常に楽しかったです。

*1:Stanza - A Python NLP Package for Many Human Languages https://stanfordnlp.github.io/stanza/

Monoxer Intern Report #1_ファイル検索機能の追加(sakabe)

はじめに

モノグサ株式会社でソフトウェアエンジニアとして約2ヶ月インターンさせて頂きました、sakabeです。名古屋工業大学で情報系の学科に所属しており、ネットワークに関心があります。

なぜモノグサのインターンに参加したのか

モノグサ株式会社との出会いは、AtCoderJobsでした。私は競技プログラミングに取り組んでいて、AtCoderのコンテストに毎週末参加していました。普段から取り組んでいるコンテストサイトにインターンの求人があり、競技プログラミングを取り組んでいることを評価して頂けそうだと思いました。 面接の段階でいくつか取り組む課題を提示して頂き、その課題に取り組んでみたいと思ったので参加を決めました。

モノグサでのインターンで主に取り組んだことと、その中で得た学びや気づき

取り組んだこと

私が担当したのは、共有ライブラリ(先生が生徒に配信する問題や学習計画、小テストを管理する画面)の改善でした。いくつか小さな課題に取り組みましたが、メインテーマはファイルの検索機能の追加でした。 問題背景として、一部の組織では大量にファイルを作成し共有ライブラリに格納しているので、目当てのファイルを探すのに時間がかかることがありました。 最初に思いついたのは、SQLのLIKE句を使うことでした。しかし、ファイルが増えるに連れて検索に時間がかかることが容易に想像できたためすぐに却下されました。 次に思いついたのは、索引(index)を作ることです。文字列のindexの貼り方が大きく2種類あります。意味ごとに分ける形態素解析と、文字数ごとに分けるN-gramという方法です。簡単な例を紹介します。

f:id:corp_monoxer:20211006164942p:plain

形態素解析では日本語の辞書を元に分かち書きします。そして、単語ごとにindexを作成します。N-gramでは、1文字ずつずらしながらN文字ごとindexを作成します。上記の例だと、京都という検索をかけた時に形態素解析では見つかりませんが、N-gramでは見つかります。 ファイルの検索という用途では、固有の文字列が多いことや検索の漏れが少ない方が良いという理由でN-gramを採用しました。このように、実装方針が立っていない課題を与えられ、解決方法を自分で探すことから始まるので、とてもやりがいが大きかったです。

f:id:corp_monoxer:20211006164952p:plain

得た学び

バックエンド、フロントエンドと役割を分けているのではなく、機能ごとにチームが分かれているので2つの言語を使いました。どちらの言語も初めて触りましたが、簡単な課題から取り組むことで徐々に言語に慣れていきました。 チーム開発は初めてで、大規模なコードはどこから編集していいのか戸惑いました。メンターの方に、コードの探し方を教わり、少しずつコードが読めるようになりました。コードのレビューも丁寧で、他の人が読んだときにわかりやすいコードが書けるようになったと思います。なぜそのように書くのか疑問に思ったときに質問をすると、納得する回答が得られました。

モノグサの社員やメンター、他インターン生とのやりとり

隣に座ったインターン生と何度か話すことがありました。他のインターン生もおやつ会を通してコミュニケーションを取ることができました。ここで驚いたのは、それぞれ取り組んでいることが違うことです。どんなことに取り組んでいるのかとても興味深かったです。 お昼にフリースペースでご飯を食べていると話しかけてくださる社員さんもいて、キャリアの相談に乗って頂きました。モノグサには中途採用の方が多いので、前職の話や転職活動についての貴重なお話を聞かせて頂きました。 メンターの方には、毎日1on1をして頂きました。わからないことを質問すると、すぐに答えが帰ってきたのでとても心強かったです。

インターンを通じての感想

ほとんど開発経験もない私ですが、このようなインターンを体験させて頂けて大きく成長出来ました。本物のチーム開発を体験できたことで、個人の開発では、気にしなかったことにも注意が向くようになりました。気軽に質問をする雰囲気を作って頂いたメンターさんには特に感謝しております。

Monoxerで画像や音声が付いた問題をインポートする仕組み

モノグサ開発チームの伊東です。今回は、MonoxerのBookをCSVファイルから作成する時に、画像や音声ファイル(以下メディアファイルと呼称)をシステムの内部でどのように扱っているかについて説明します。

Bookインポート機能とは

BookとはMonoxerにおける問題集のようなものです。MonoxerではBookを作成する主な方法として、大きく分けて2通りを提供しています。

  • WEB管理画面上で作成する方法
  • CSVファイルを作成しておいて、それをWEB管理画面から読み込ませる方法(Bookインポート機能)

WEB管理画面上で作成する方法は、ブラウザ上でマウスとキーボードをつかってBookを作成できるので比較的始めやすい方法です。

CSVファイルを作成する方法は、定められたフォーマットのファイルを書く必要がありますが、大量に問題を作成したい場合は、便利な方法です。特に同じ形式の問題をたくさん作る場合は楽になります。

【参考】

モノグサ管理マニュアル:Book(問題集)を作成する

※Bookインポート用のCSVファイルの例

・通常の英単語など

・音声を使うBook

・画像を使うBook

Bookインポート機能でメディアファイルを使いたい場合、CSVファイルとは別に、メディアファイルをアップロードする必要があります。 CSVファイルでBookを作成する前に、まずメディアファイルをアップロードしておきます。 CSVファイル中には、アップロード済みのファイル名を書くことで、ファイルを指定する仕組みにしています。

www.notion.so 使用するCSVファイルを①にインポートします。画像や音声を使う場合は②にインポートします。([.jpeg]と[.mp3]に対応しています) f:id:corp_monoxer:20211001171217p:plain

※ファイル一式をZIPファイル1つにまとめてアップロードするという方法も考えられますが、Bookを作成するユーザにとっては、ZIPファイルにまとめる手間がかかります。 CSVファイルは、Book完成までに何回か試しにインポートしてみて、修正を繰り返すのが普通ですので、その度にZIPファイルのまとめるのは面倒すぎます。 (実際のところサービスを実装する上でもファイルの処理が煩雑になって面倒そうです。)

このあたりの仕組みについてもう少し詳しく説明していきます。

アップロードされたファイルは、クラウド上のストレージ(オブジェクトストレージ)に格納されます。この際に、元のファイルの名前を使わず、新しいファイル名を作ってストレージに保存しています。なぜ元のファイル名を使わないのでしょうか?

名前を付け替えないと?

元のファイル名のままストレージ上に保存するというやり方だと、以下のような不都合があります。

  • 名前が容易に衝突する。そして上書きされると、既存Bookが壊れる。
  • 上書きされないような名前をユーザに付けてもらうのは不便
  • 中身が同じ別名のファイルが発生しうる

作成中のBookについては、一度アップロードしたファイルの差し替えが出来ることは望ましいのですが、以前に作成したBookには影響を及ぼさない方が良い。というわけで、名前を付け替えるのがよいです。

※インポート前に予測可能な規則を定めて、ユーザがアップロード時にファイル名を付与する方法も考えられます。例えば、エントリ番号を名前に付けて123.jpegなど。これはCSVファイル中でファイル名を書かなくても良くなるかもしれない(規則的なファイル名を暗黙的に指定できる)という利点がありますが、 以下のような欠点がありそうです。

  • ユーザがBookに対応したファイル名をBook毎につけ直すのは面倒
  • インポート前に毎回ファイルをアップロードし直す必要があるかもしれない
  • 実質的に同一のファイルを、システム上で同一視できない
  • 中身が同じファイルでもストレージ上で重複して保存される

さて、新しいファイル名の作り方です。すぐ思いつきそうなやり方としては、

  • 連番を振る方法
  • ランダムなファイル名を付ける方法

が考えられます。

これでファイル名が重複することは(ほぼ)ないのでよさそうな方法ですが、これらは事前に予測が難しいという問題点があります。これらをCSVファイル中にそのまま指定することは現実的ではありません。 インポートでメディアファイルを指定するためには、CSVファイルを用意する段階で、ファイルを特定できる情報(ファイル名)が予測可能であり、CSVファイル上で指定できる必要があります。 また、同じファイルを何度もアップロードした場合でも、別の名前のファイルとして、ストレージに保存されることになります。ストレージ容量が無駄になります。

同一オブジェクトの同一視と、重複排除の手法

オブジェクトストレージ上のオブジェクトのファイル名(以降、オブジェクト識別子)としては、中身が同じファイルを複数保存しないためには、実質的に同一とみなすべきオブジェクトの持つ特徴的な情報を識別子として利用するのがよいです。

今回はオブジェクトの本体のダイジェストハッシュ値を用いることにしました。つまり本体がバイト単位で完全一致した場合に同一オブジェクトとみなす方法です。

これにより、いつアップロードされたかにかかわらず、中身がファイルならば、システム内部で同じオブジェクトであるという扱いができるという利点があります。 これによりシステム内部で同一オブジェクト(画像や音声)は同一視されます。

  • オブジェクト識別子は、Bookの中で記憶度を検索するためのキーの一部としても利用されているため、同一のオブジェクトとその記憶度とを一対一対応させることができます
  • ストレージ上で重複排除されます

※実装ではハッシュ関数としてSHA256を用い、同一でないオブジェクト同士で識別子の衝突が発生することがないように留意しています。同様の手法はgit等で広く使われています(gitではSHA1)。

f:id:corp_monoxer:20211001171238p:plain
オブジェクトストレージ上の画像を入れるバケツ

インポート時のメディアファイルの特定方法

ユーザがCSVファイルを用意する時に、事前にアップロードしたときのメディアファイル名を、CSVファイル中に指定してもらいます。 そして、Bookインポート時に、CSVファイル中に指定されたファイル名を、ストレージ上のオブジェクト識別子に読み替えます。このために、ファイル名とオブジェクトの対応表を用意しています。この対応表は、メディアファイルをアップロードした段階で更新してあります。

f:id:corp_monoxer:20211001171248p:plain
ファイル名とオブジェクトの対応表

一度作ったBookの中の画像や音声は、サーバ上ではオブジェクト識別子(≒ストレージ上のファイル名)を使って表現されています。(元のファイル名とストレージ上のファイル名の対応表を参照して作成されます。) この仕組みにより、Bookインポート後のメディアファイルアップロード操作の影響は受けません。つまり同一ファイル名で、中身が異なるファイルをアップロードしても作成済みBookの画像や音声には影響を及ぼしません。

もし、画像や音声を変更したい場合は、別のファイル名でアップロードし直して、CSVファイル中でその別のファイル名を指定することで差し替えることができます。

メディアファイルのアップロード機能と、CSVファイルのインポート機能は、それぞれ独立しています。つまり、一度アップロードしたら、ブラウザをリロードしたりしても影響せず、その後のインポートで何度でも利用可能です。したがって、一度メディアファイルをアップロードすれば、あとは、CSVファイルだけ修正とインポートを繰り返すことができます。

もちろん、都度メディアファイルアップロードしてもらっても問題ありません。 上で説明したように、中身が同じファイルは1個だけオブジェクトストレージに保存されます。さらに、クラウドサーバでは一般的にアップロード方向の通信量には課金されないのでサービス運用側としては同じファイルを何度アップロードしてもらっても特にデメリットはないのです。

Bookインポート機能の内部処理のまとめ

1.メディアファイルのアップロード時

  • オブジェクト識別子を決定する(ダイジェストハッシュ値を計算)
  • オブジェクト本体をストレージに保存する
  • ファイル名とオブジェクトの対応表に、ファイル名とオブジェクト識別子を登録する

2.CSVファイルによるBookインポート時

  • ファイル名とオブジェクトの対応表を参照して、CSV中で指定されたファイル名から、オブジェクト識別子を取得する
  • Bookで使うオブジェクトをダウンロードするためのキーとして、オブジェクト識別子を使う
  • 記憶度を保存、検索するためのキーの一部として、オブジェクト識別子を使う

今回は、Bookインポートでメディアファイルを使う仕組みについて解説しました。私からは以上です。

文系出身の私がモノグサでエンジニアを始めた理由

f:id:corp_monoxer:20210905154159j:plain

モノグサ株式会社のTobisatis(@tobisatis)です。2021年2月にソフトウェアエンジニアとして入社しました。

当社には大学や大学院で計算機科学を専攻していたような、技術的に非常に優秀なエンジニアが多く集まっています。そんななか、計算機科学どころか理系ですらない、経営学部出身の私がモノグサで働きはじめる話をしたいと思います。

大学卒業まで

高校卒業まで、私は理系の生徒でした。といっても、理科や数学が得意だったわけではありません。在学中、理系から文系は変えられるけれどその逆は難しいと聞いたからでした。理科の中でも物理ではなく生物を選んでいたので、典型的な理系の生徒像とは少し違うかもしれません。

高校時代に勉強していた生物は割と楽しかったので、特に根拠もなく「iPS細胞を応用して、食糧問題や環境問題、エネルギー問題などのあらゆる問題を解決する!」と意気込むようになりました。大学受験は第一志望に落ちたため、合格した大学の経営学部に進学することにしました。

大学生活は特に変わったことをせず、しばらく過ごしていたのですが、大学4年でいわゆるレールから外れることになりました。

大学での社会科学の勉強を通してさまざまな思考を巡らせた結果、大雑把に表現すると「裕福でない子どもたちにも、よりよい教育を提供する必要がある」という結論に達し、これに使命感を抱くようになりました。しかし、この目標はビジネス的にはあまり歓迎されるものではないと気づき、就職によってこれを達成しようとすることはやめました。新卒での就職をしないことに決めたのです。

就活をせずに時間があったため、なんとなく始めたことがありました。それが競技プログラミングです。競技プログラミングとは、与えられた問題を解くためのプログラムを提出して得点や速度を競う競技です。これまで、確率の問題を解くためにJavaScriptを使って簡単なシミュレーションをしたり、無料レンタルサーバーを使ってPHPで遊んでみたりとプログラミングに触れる機会があったので、競技プログラミングを始めたのは自分にとって自然な成り行きだったな、と思います。

大学卒業後

先のような使命感から、「自分で塾を経営する」という計画を立てていました。しばらくの生活費を稼ぐためにフリーランスのPHPエンジニアとして8ヶ月ほど働いたのち、2020年5月、私の両親が神奈川県茅ヶ崎市で経営する「カフェのあるめがね屋さん一丁目四番地」内で「総合教室Salite(サリーテ)」をオープンしました。

しかし、新型コロナウイルスの影響もあり、宣伝が思うようにできませんでした。約1時間のマンツーマン授業を990円で受講できる破格のプランを掲げていても、問い合わせはなかなか増えませんでした。この塾の人気が上がる見込みを期待できない状況だったため、悶々とした日々を送っていました。

モノグサとの出会い

そんななか、Wantedly(ビジネス向けSNS)を経由して、モノグサ株式会社から直接、ソフトウエアエンジニア採用のお誘いをもらい、「教育の業界を少しでも勉強できたらラッキー」くらいの軽い気持ちで面接を受けることにしました。

そのときは、あっけなく落ちるだろうと思っていました。チームでの実務経験が高々8ヶ月。塾の営業に必要なホームページやカレンダー関連のシステムは自作していたものの、職業エンジニアとしてはブランクがある状態。転職市場ではほとんど評価されない経歴だろうと根拠はないものの感じており、胸を張って面接に臨むことはできませんでした。

結果的に内定を得ることができ、そのときは信じられませんでした。技術面接ではひたすらに自分のエンジニア領域の知識のなさを気付かされ、恥ずかしさのようなものだけを感じながら帰路についた覚えがあります。ただ、そんななか振り返ると、競技プログラミングで培った問題解決能力が高く評価されたのだろうと感じています。

競技プログラミングで得た力

私はAtCoderというサイトで競技プログラミングに参加していました。AtCoderにはレーティングのシステムがあり、定期的に開催されるコンテストで良い結果を出すほど高いレートが付きます。コンテストは出題される問題の難易度によって、ABC、ARC、AGCという種類に分かれています。

コードを書くのはとても遅いものの難しい問題の考察に取り組むことが好きだった私は、最も難易度の高いAGCに出場して2問だけ選んでじっくり解く、という戦略をよくとっていました。

業務における開発では、ひとつの問題にじっくりと取り組む力が重要です。このため、大学院で熱心に研究に打ち込んでいるような学生は、モノグサでも高く評価される傾向にあります。私は大学院に行っておらず、学部での研究も卒業論文の提出の他にはなかったのですが、競技プログラミングを通してこの力を身につけることができました。

f:id:corp_monoxer:20210905153625j:plain

入社を決めた理由

モノグサのエンジニア採用は、技術面接をすべて通過すると最終面接に移る、というプロセスです。最終面接ではエンジニアだけでなく、さまざまな部署のメンバーと面接をします。

就職をすることは、そもそも私にとって目的ではありませんでした。したがって、技術面接を通過したあとも、面接の準備や練習を一切することなく最終面接に行きました。

最終面接では、自分がどのような思いで塾の先生をやっているか、すなわち、先に述べたビジネス性がないと結論付けた目標をひたすら正直に語りました。おそらくこのときは生徒が多く集まらない孤独感から話を聞いて欲しい思いが強くなってしまっていたのだろうと振り返ると、顔が赤くなります。

モノグサに入社を決めたのは、内定をもらったからに他なりません。メールボックスにモノグサからの内定通知が届いたとき、道を間違ってはいなかった、と感じたのを覚えています。

現代に生まれたものとして、掲げる理想だけでも高潔でいたい。経済的な成長のためだけにそれを犠牲にしたくない。こんな思い全てを明かしても私を迎え入れてくれるこの会社でなら、自身の尊厳を毀損せずに生きられるだろう。

そう確信し、私は入社を決めました。

入社してからは、周りのエンジニアの優秀さに圧倒される日々が続いています。

それでもできることを自分なりに探しながら、「記憶を日常に。」のスローガンの元で自分の信念に基づいて仕事をできている今は、過去になく充実しています。

『記憶を日常に。』を実現するための大切な仕組み。Monoxerのオフライン学習とは

モノグサエンジニアの井岡です。 モノグサでも技術的な情報を技術ブログとして発信していきます。プロダクトのMonoxerに関連することだけでなく、モノグサ社員が趣味で行っている内容等についても紹介していく予定です。

記念すべき第1回は、Monoxerの特徴的な機能であるオフライン学習についてご紹介します。

Monoxerとは

Monoxerは解いて憶える記憶アプリです。憶えたい内容を登録すると、記憶している状況(=記憶度)に応じてアプリが問題を生成します。まったく憶えていないときは写経形式(薄く答えが出ている状態で回答)で、少し憶えているときは択一形式で、すでに記憶していそうであれば自由入力形式で、といったように記憶度に応じて出題形式が変化します。そのため無理なく自然に憶えることができます。

出題形式をコントロールするための重要な機能として記憶の一元管理*1があります。簡単に説明すると、問題を要素に分解し、それぞれの記憶度を管理するための機能です。例えば、数式の「2+3」であれば整数「2」、「3」と、「足し算」の要素に分解できます。これらを記憶している状況で、「3+4」を出題する際には、整数「3」と「足し算」は記憶しているとみなして、出題形式を決定しています。

管理機能も充実しており、塾や学校の先生が生徒に課題(=タスク)を配信できます。配信したタスクを生徒が学習すると、その学習結果や記憶度を逐次サーバーに送信していきます。そのため、生徒ごとの進捗や記憶度をリアルタイムに確認することができます。

f:id:corp_monoxer:20210819163503p:plain

オフライン学習の仕組み

Monoxerを用いた記憶は日常的に行われるものであり、飛行機や地下鉄の中、山でのキャンプ中等のように通信が不安定な環境下でも学習できる必要があります。もちろん、タスク配信時にはサーバーからデータを取得する必要がありますが、学習については可能な限りオフラインでも実行できるような仕組みにしています。

Monoxerでの学習は以下のような流れで行います。

  1. サーバーからタスクをダウンロード (初回のみ)
  2. タスクを学習
  3. 学習結果・記憶度をローカルに保存
  4. 学習結果・記憶度をサーバーに送信 f:id:corp_monoxer:20210819163527p:plain

サーバーに確実にデータが保存された状態にするために、データがサーバーに保存されたかどうかをdirtyフラグを用いて管理しています。ローカルに保存するタイミングではdirtyフラグをtrueにし、サーバーへの保存が成功した場合にfalseに変更します。オフライン時には上記4.のサーバーへの送信が失敗するため、dirtyフラグがtrueのままローカルに保存された状態になります。 f:id:corp_monoxer:20210819163530p:plain

その後、端末がオンラインに変わったタイミング*2で、ローカルに保存されている学習結果や記憶度のうちdirtyフラグがtrueのものをすべて取得し、あらためてサーバーに送信します。

f:id:corp_monoxer:20210819163523p:plain

また、記憶度は最新のものがサーバーに保存された状態になっています。Monoxerでは複数端末での学習も可能なため、保存されているものと受け取ったもののうち新しいものが保存されるようにタイムスタンプのチェックも行っています。

他にもサーバーでの保存は成功しているが、レスポンスを受け取れないということも起こり得ます。この場合、ローカルに保存されている学習結果のdirtyフラグはtrueのままであるため再度サーバーにデータが送信されますが、サーバーではすでに同じデータが保存されているため、重複して保存されないようにする必要があります。そのため、サーバーで管理しているIDとは別に、ローカルでユニークIDを生成し、これを比較して保存済みかを判断しています。

まとめ

Monoxerでは日常的に記憶するために様々な機能を提供しています。今回はその中のオフライン学習についてご紹介しました。様々な状況での学習に対応するために、整合性を取れるようにデータを管理しています。 他の機能については、別の機会にご紹介できればと思います。

*1:記憶の一元管理の詳細はCTOのインタビュー記事をご確認ください。

*2:iOSではReachabilityのwhenReachableおよびwhenUnreachableを用いて通信可否の変更を検知しています。Androidではandroid.net.ConnectivityManagerのNetworkCallbackを用いて通信可否の変更を検知しています。