Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

UNIX 系システムのプロセスに関する社内勉強会に参加しました

こんにちは。新卒 1 年目エンジニアのすずまさです。

先日、弊社 VPoT の id:nkgt_chkonk が執筆した process-book の勉強会を修了しました。

process-book は *nix系のシステムにおけるプロセスやシグナルなどについて説明することを目的に書かれました。「プロセスとかよくわかってないからちゃんと知りたいな」みたいなひとたちが想定読者です。

参加する前は UNIX の基礎的な知識も乏しかった私ですが、学びがたくさんあり毎回楽しく参加できたので紹介します。

process-book の社内勉強会の様子

開催の経緯

Slack でシェルスクリプトの話題で盛り上がったのをきっかけに、プロセスモデルの重要性が話に挙がり、流れでその日のうちに「process-book 読もう会」が誕生しました。

進め方は下記の通りです。

  • 日時: 毎週木曜日 15:00〜15:30
  • 週に 1 章ペースで進める
  • 初めに各章ごと担当者を決めておき、担当者は esa に内容をまとめ、当日司会をする
  • 参加者は各自手を動かして予習しておく
  • 疑問や気付いたことがあれば会の中で発言する

印象的だった章

4 章のファイルディスクリプタの話が印象的でした。

  • OS は、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開きます。
  • OS は、その開いたファイルを表す「番号札」を作成します。
  • OS は、その番号札をプロセスに対して返します。

この「番号札」のことを、「ファイルディスクリプタ」と呼びます

Ruby を使い、実際にファイルディスクリプタを出力してみると "5" が出力されます。

また、標準入力のファイルディスクリプタは 0、標準出力は 1、標準エラー出力は 2 ということを学びます。

ここで、「では 3 と 4 は何を指しているのか?」という疑問が生まれました。

会の中では「lsof コマンドを使うと調べられるよ!」とだけアドバイスを頂けたので、勉強会後に実際に実行してみると、下記のような結果が得られました。

# fd.rb
file = File.open("nyan.txt","w")
puts file.fileno
puts $$
while(1)
  sleep
end
$ ruby fd.rb &
$ 5 # ファイルディスクリプタ
8300 # プロセスID
$ lsof -p 8300
COMMAND  PID     USER   FD      TYPE DEVICE SIZE/OFF   NODE NAME
~~~ 省略 ~~~
ruby    8300 ssm-user    0u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    1u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    2u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    3u  a_inode   0,14        0     13 [eventfd]
ruby    8300 ssm-user    4u  a_inode   0,14        0     13 [eventfd]
ruby    8300 ssm-user    5w      REG  259,1        0 259116 /home/ssm-user/004/nyan.txt

ここで、FD (ファイルディスクリプタ) の 3u と 4u を見れば良さそうだと分かりますが、これだけ見ても実際に何が行われているのかは分かりません。 そこで、man eventfd を実行してみると、下記のような文章が得られます。

eventfd() creates an "eventfd object" that can be used as an event wait/notify mechanism by user-space applications, and by the kernel to notify user-space applications of events.

この文章から、ファイルを読み込んだ後にそのファイルへのアクションを待つのが 3 で、アクションを受け取って通知するのが 4 だと予想できます。

では、その通知はどこに向けてされているのでしょうか?

Ruby の実装を読んでみましょう。

pipes[0] = pipes[1] = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC); https://github.com/ruby/ruby/blob/b6c1e1158d71b533b255ae7a2731598455918071/thread_pthread.c#L1767

この文が呼び出されているコメントを読むと、"communication pipe with timer thread and signal handler" と書かれています。 つまり、通知を出しているのは Ruby 内部のタイマーで、通知を受け取っているのはシグナルハンドラーなのだと分かります。

上記は勉強会後に Slack で質問したことで得られた学びなのですが、「ファイルディスクリプタの 3 と 4 が何を指しているのか知りたい」という当初のシンプルな疑問から Ruby の実装を読むことになるとは思わなかったので感動しました。

1 つの疑問から新たに疑問が生まれ、それがどんどん解消されていくのが気持ち良くて楽しかったです。

参加して嬉しかったこと

質問が歓迎される

本勉強会では先生役のような詳しい人が何人かおり、その方たちに出てきた疑問をよくぶつけていたのですが、たまに「良い疑問ですね」と言われるのが嬉しくてかなりモチベーションになりました。

疑問をぶつけるとその場で解消され、解消されなかったとしても考察が広がるので、疑問を持てば持つほど楽しい勉強会でした。

man を読むようになった

man はman コマンド名でそのコマンドのマニュアルを読むことができるコマンドです。

正直今まで man コマンドを使ってマニュアルを読んだことがほとんどなかったのですが、今回の勉強会を経て man をしっかり読むようになりました。

man を読むことで疑問が解決できたり、新しく洞察が得られたりすることが多く、改めて公式ドキュメントの重要さを認識しました。

ボリュームがちょうど良かった

文字数が多くないので予習が苦ではありませんでした。 余裕があったため、章の内容通りに手を動かすだけでなく、毎回気になった箇所をじっくり調べることもできました。

感想

最初の方は分からないことが多すぎて議論についていくので精一杯でしたが、途中からわかることが増えて様々な疑問が浮かび、そこから一気に楽しい勉強会になりました。

他の方の鋭い疑問に驚く場面も多く、一人でやっていたらここまで学びは多くなかったと思うので、本当に参加して良かったです。

開催してくださった先輩やサポートしてくださった方々には感謝の気持ちでいっぱいです。

今回学んだ内容を活かした発展的な内容もやりたいという話が挙がっているので、次回があれば是非また参加したいと思います!

Amazon OpenSearch Serviceをアップグレードしました

こんにちは、プロダクト開発部でバックエンドエンジニアをしている望月です。

Classiのサービスでは、先生や生徒がアップロードしたコンテンツファイルやWebテストなどの検索システムにおいてAmazon OpenSearch Service(以下、OpenSearch)を利用しています。
コンテンツファイルのメタデータやWebテストのデータ等はRDBでも保持しているのですが、以前検索におけるパフォーマンスが課題になった際にOpenSearchが導入されました。 ユースケースとしては、Webテストの文章からの全文検索や、ファイル名・(オーナーの)ユーザー名等でのキーワード検索、カテゴリや属性ごとの検索などがあります。

今回はこのOpenSearchのアップグレード実施で行ったことや学びになったことをお話ししていきます。

アップグレード方法

AWSがマネージドサービスとして提供しているインプレースアップグレードがありますが、当時のOpenSearchのバージョンが古く対応していなかったため、新しくOpenSearchドメインを立てて移行する形を取りました。また、同時にOpenSearchを配置するネットワークの移行も実施しました。

今回、新旧OpenSearch間で 移行したドキュメント数は約572万件でした。Classiの中でも比較的データ量の多いシステムで、移行に30時間程度必要という見積もりだったため、定期深夜メンテナンス(5時間)の中でデータ移行を含めた作業を完結させるのは厳しいという判断になりました。

新旧OpenSearchを並行稼働させて両方に書き込みを行う期間をもった上で切り替えるという案もありましたが、この登録処理でパフォーマンスに課題があり、処理を増やしたくないという理由で今回は採用に至りませんでした。

最終的に実施したのは、事前にデータ同期済みの新しいOpenSearchドメインをスタンバイさせ、定期夜間メンテナンスでは新旧OpenSearchの切り替えと、アップグレードに対応したクライアントアプリケーションのリリースのみを行う、という方法でした。

本番オペレーションの手順を詳細に書いてデモンストレーション

本番オペレーションは、以下のような手順で進めました。

  1. 新しいOpenSearchへ、事前に既存のデータを取り込む
    • RDBのデータをもとに、クライアントアプリケーションから 新しいOpenSearchへデータを再登録するスクリプトを実行
  2. 定期深夜メンテナンスまでは通常通りユーザーがサービスを利用しているため、新旧OpenSearch間で発生するデータ差分を定期的に同期
    • RDBのデータの更新履歴をもとに、クライアントアプリケーションから 新しいOpenSearchへデータを更新するスクリプトを定期的に実行
  3. 定期深夜メンテナンスで新旧OpenSearchを切り替え
    • 新しいOpenSearchのバージョンに対応したクライアントアプリケーションのリリース
    • クライアントアプリケーションが新しいOpenSearchへアクセスするよう切り替え
    • QAの実施

大量のデータを扱うことには個人的にこれまで苦手意識があり、検証段階ではデータ再登録・更新時のスクリプトで意図しないタイムアウトが発生するなど苦労もありましたが、本番のオペレーションは大きな問題もなく終わらせることができました。

前述の通り、今回のOpenSearchアップグレードは定期深夜メンテナンスで作業を行う必要がありました。そして、定期深夜メンテナンスはおおむね月に一度の実施となっているため、本番でチャレンジできるタイミングが限られていました。
そのため、事前にオペレーション手順(データ移行・新旧OpenSearch切り替え・クライアントアプリケーションのリリース・ロールバック)をすべてチェックリスト形式で詳細に記述しておき、ステージング環境にてQAを行う際に何度かオペレーションの練習をしました。AWS環境に反映してみて初めて表出した事象もいくつかあったため、都度対応しながらオペレーション手順へ取り込んでいきました。

結果的にロールバックは実施しませんでしたが、手順を詳細に書いておくことで当日に落ち着いて作業を進められました。また、移行後のデータに追加で処理を走らせる際にも役立ちました。

システムと組織の依存関係に合わせてメンバーを巻き込む

今回最も難しかったのは、OpenSearchからユーザーまでの間に複数の機能コンポーネントが存在していて、それぞれ担当するチームが異なっていたことでした。 大規模なシステムの場合、機能コンポーネントを複数扱うプロジェクトを進めていくのは技術的にも困難ですが、関係するメンバーの意向を確認しつつ進めていくことも大きな課題となるのではないでしょうか。

今回のケースですと、各機能コンポーネントを担当するチームは以下の通りでした。

  1. OpenSearchを管轄するチーム
  2. OpenSearchのクライアントアプリケーションを管轄するチーム
  3. ユーザーから見える機能を管轄するチーム

最初私は1のチームメンバーとして作業を進めていましたが、検証環境を作るにあたっては2のチームメンバーに相談相手やレビュワーになってもらいました。また、リリースするにはユーザーから見える機能がちゃんと動いているかどうか確認するプロセスが必要だと思い、3のチームメンバーに協力をお願いしたり、自分がチーム移籍したりして作業を進めました。
また、OpenSearchの切り替え後に発覚したデータ不整合において今回の移行が起因であるかどうかの調査を行うことがあり、こちらもチーム間で連携して過去の経緯を追いつつ調査を行いました。

最後に

以上、OpenSearchのアップグレード実施についてお話しさせていただきました。 今回、自分1人で抱え込まずに適切なチームにヘルプを依頼できたのは、振り返ってみると良かったのではないかと思います。力を貸してくれたメンバーには、本当に感謝しています。

今後もアップグレード対応は継続的に行なっていく必要がありますが、今回実施した際の知見を活かし、よりスムーズに行えるようにしていきたいと思っています。

We are Hiring!

Classiでは今回紹介した以外の機能ですと、学校内でやりとりするメッセージ機能や、ポートフォリオ(活動記録)の検索システムでもOpenSearchを利用しています。 また、他にもECS化やデータ基盤の活用など技術的に面白いトピックも多くあります。詳しくはぜひ、カジュアル面談等でお話ししましょう!

Jest v28 shard オプションを使い、CI でカバレッジを計測できるようにする

こんにちは、ラーニング・学習トレーニングチームの id:tkdn です。 今日は Jest shard オプションを使って CI でどうカバレッジ計測をしたか について書いていきます。

Jest v28 shard オプション

Jest v28 から shard オプションが入りました。このオプションでテスト実行を指定の数で分割することができます。

Jest's own test suite on CI went from about 10 minutes to 3 on Ubuntu, and on Windows from 20 minutes to 7.

公式のアナウンスでもあるとおりですが、Jest 自身の CI でのテストが 20 分から 7 分になったという速度の変わりようです。

私たちのチームではリモートという状況の中で TDD /モブプロを実践しており、コミットによりバトンをつないでいます。そのため CI のスピードアップは開発体験を良くするだけでなく、マクロな視点ではリリースのリードタイムを短くすることにもつながります。

shard オプション、これを使わない手はありません。

shard オプションの使い方

我々は GitHub Actions を使っているので以下のように設定ファイルを書き換えました。特に難しいところはありません。ワンラインの実行オプションについては Jest のドキュメントに記載があります

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}

node_modules のキャッシュなどほかのステップは省いていますが、最小構成であれば上記の設定で動きます。これで testジョブは 4 つ並列で動くことになります。

環境にもよりそうですが、我々の環境では約 500 のテストケースで 5 分弱かかっていたところ、shard で 4 つ並行させると半分の 2 分半に短縮できました。 めちゃくちゃインパクトがあるわけではないのですが、ごくまれにモブプロで全体通してテスト結果みたいねというときや、ちょっとした修正後のデプロイなど、待ち時間が短くてうれしいタイミングはいくらでもあります。

shard オプションで実行した後カバレッジをどうするか問題

ただし問題が出てきます。プロジェクトでは coverageThreshold を設定していたため Jest が機械的に分割したテストのサブセットではグローバルなカバレッジ閾値を満たすことができないという状況が発生しました(分割せずに実行した場合はもちろんカバレッジを満たします)。

並列テスト A のコードパスで通ったモジュールやコンポーネントが必ずしも並列テスト A でテストされている保証はないので、そうなるだろうという予想はなんとなくしていましたが…。

類似した Issue: [Bug]: shard option and global coverageThreshold config · Issue #12751 · facebook/jest

解決策:並列テストのカバレッジを別ジョブで統合する

この問題についてはチームメンバーが解決策を持ってきてくれました。

参考になる Issue: Expose istanbul/nyc's check-coverage functionality in jest · Issue #11581 · facebook/jest

まずは並列で実行したテストのカバレッジを個別で Artifact として持っておきます(Artifact 自身は次のジョブで使います)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+   - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
+   - name: Upload Artifact
+     uses: actions/upload-artifact@v3
+     with:
+       name: tmp-coverage
+       path: ./coverage

次に新しいジョブで並列実行により得られた Artifact を集めて nyc を使ってマージし(A)、nyc を使ってカバレッジ計測し(B)、簡易的なスクリプトでカバレッジを満たしていないファイルなどを出力します(C)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --coverage --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
    - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
    - name: Upload Artifact
      uses: actions/upload-artifact@v3
      with:
        name: tmp-coverage
        path: ./coverage
+ report-coverage:
+   needs:
+     - test
+   runs-on: ubuntu-latest
+   steps:
+     - uses: actions/checkout@v3
+     - uses: actions/download-artifact@v3
+       with:
+         name: tmp-coverage
+         path: tmp-coverage
+     - name: Merge coverage
+       # (A)ここで並列で得られたカバレッジをマージする
+       run: npx nyc merge tmp-coverage/ coverage/coverage.json
+     - name: Check coverage
+       # (B)マージしたカバレッジ計測判定を行う
+       run: npx nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100 -t coverage/
+     - name: Report coverage
+       if: failure()
+       run: npx nyc report --reporter=text -t coverage/ > coverage/coverage-result
+     - name: List Low coverage files
+       if: failure()
+       # (C)CIでの出力のためにスクリプトを実行します
+       run: node .github/tools/extract-low-coverage.js

これで大方やりたいことは実施できました。

注意点

気にしなくてはいけないこととしては、GitHub Actions の従量課金は実行時間の合計が対象となることです。実行時間が半分になり嬉しい限りなのですが、2 分半 * 4 並列が実際の実行時間となる点はしっかり踏まえて、お使いのプラン内の無料枠を考慮しましょう。少なくとも最初から並列する数を多めに設定しすぎてクォータを食いつぶした! なんてことがないようにするとよいですね。

まとめ

Jest v28 から使える shard オプションとそれを利用したカバレッジ計測について書きました。参考記事として以下に挙げた URL で狂喜したのですが、shard で実行後のカバレッジ計測のプラクティスがなかったので今回筆を取らせていただきました。

shard オプションは嬉しいのですが、実際には Jest のカバレッジの仕組みでフォローできるようになると良いですね。

Classi では CI の速度が気になった際にカッとなってやってしまうエンジニアを歓迎しています!!

参考 URL

ClassiのアダプティブラーニングエンジンCALEの品質を高める取り組み

はじめに

こんにちは、Pythonエンジニアをしてます工藤( id:irisuinwl )です。 この度、Classi独自のアダプティブラーニングエンジンである Classi Adaptive Learning Engine (CALE) をリリースしました。

corp.classi.jp

自分は主にCALE開発において、レコメンドを行うエンジン部分のバックエンドを担当しました。 今回の記事では、0からレコメンドシステムを開発し、システムが安定稼働する品質を実現したノウハウを紹介したいと思います。

CALEの概要

アダプティブラーニングとは、個別最適化学習のことを言います。 CALEでは、従来のClassiでのテスト機能である先生から生徒へのテストを配信、テスト解答に加えて、テスト終了後に生徒のそれぞれの理解に応じて問題を出題し、そのテストについて生徒それぞれの理解を深めるための機能を実現しました。

アーキテクチャ

CALEは Google Cloud (Google Cloud Platform, 以下 GCP) 上で構築されており、アプリケーション基盤としてGoogle Kubernetes Engine (以下 GKE) を使っております。

推論するためのモデルとAPIのpodは分離し、モデル開発とAPI開発の責務分割および、将来的に様々なモデルによって推論できるように設計をしました。

レコメンドエンジンのAPI部分はFlaskを用いて実装しており、ストレージには Firestore, Cloud SQL, Cloud Storage を利用しております。

工夫した点

CALEは新規システムであり、個別最適化学習を実現するレコメンドエンジンの要求仕様や、良いレコメンドの体験など、見えない部分が多いです。そのため、システムが安定稼働すること、つまりシステムの品質を守ることを重視しました。 以下ではシステムの品質を高める上で、工夫したTipsを紹介していきます。

本記事で紹介する取り組みを実施することで、CALEでは

  • valid rateが99.99%,
  • 95%ile latencyが779ms

という安定したシステムを実現することができました。

テスティング

テストを書くことは品質を保つ点において重要です。 CALEでは以下のテストをおこなっていました。

  • ユニットテスト
  • インテグレーションテスト
  • 負荷テスト
  • QA

ユニットテスト、インテグレーションテストではPythonのテストフレームワークであるpytestを用いてテストしました。

コードカバレッジはQAチームと相談し、C1 Coverageを選択しました。最終的にカバレッジを95%以上高めることができました。

負荷テストは、LocustというPythonの負荷テストライブラリを用いて実装しております。

各API呼び出しのユースケースに対して、今期想定されるユーザー利用数から負荷のテスト設計・実装を行い、ビジネスサイドに近いプロダクトオーナーを含めてレビューを行いました。 また、レイテンシが高まる観点として、登録したユーザー数、レコメンドされる問題数、蓄積された解答数といった、データ量を観点に入れて負荷テストの設計・実装を行いました。

Locustの詳細な使い方は以前自分が書いた こちらの記事 を参考に頂ければ幸いです。

QAではAutifyを使って、実際にユーザーを想定したユースケースのE2Eテストを定期実行しました。E2Eテストの自動化によって、アジリティ高くデプロイすることが出来ました。

運用・監視

運用・監視はCloud Operationsの内、以下を利用しました。

  • Cloud Logging
  • Cloud Monitoring
  • Cloud Profiler

特にCloud LoggingのPython clientがv3.0.0 になってからさまざまな情報を構造化ロギングすることが出来、使い勝手が良かったです。 以下のようにextra引数に値を入れることでログのjsonPayloadに値を入れることが出来ます。

import logging
from google.cloud.logging.handlers import CloudLoggingHandler
import google.cloud.logging

client = google.cloud.logging.Client(project="test-project")
handler = CloudLoggingHandler(client)
# setup_logging(handler)

cloud_logger = logging.getLogger('cloudLogger')
cloud_logger.setLevel(logging.INFO)
cloud_logger.addHandler(handler)

data_dict = {"hello": "world"}
cloud_logger.info("cloudLogger logging lib test1", extra={"json_fields": data_dict})

上のコードを実行すると、以下のようにログが出力されます。

また、運用と開発を両立するために SLI/SLO を設定しました。 GCP が提供している SLI/SLOに関するドキュメント を参考に、以下の流れで作成しました:

  • 利用ユースケースをまとめる
    • 利用ユースケース (クリティカルユーザージャーニー)の一覧化
    • ユースケースごとの影響度合いを考える
  • データ分析
    • 各ユースケースにおける処理の性能を測る
    • 現状の利用状況に対して、厳しく設定するのか、緩く設定するのかを考える
  • SLIの設計
    • 上記で洗い出したユースケースごとに設計する
    • ユーザーがサービスを問題なく使えるために見るべき数値は何かを指標に落とす
  • SLOの設計
    • ユーザーがサービスを問題なく使えるために指標をどの程度にすれば良いかを考える

そして、考えた基準をCloud Loggingのログベース指標でメトリクスを取得し、Cloud Monitoringのサービス経由でSLI/SLOに設定しました。

Kubernetesやインフラの運用テスト

開発環境で運用のテストも行いました。 テストの手順としては先述したLocustでのシナリオテストを常に行いながら、インフラの構成変更、スケーリングなどをテストし、ダウンタイムが生じるかを確認しました。 基本的には考えられる運用を列挙して、その洗い出した項目をテストをしました。 以下がその項目の一例となります。

  • 高負荷を掛けて、GKEのオートスケーリング時の挙動をテストする
  • クラスタのアップデート
  • ノードのアップデート
  • 誤ったイメージをpush, rollbackする
  • Cloud SQLのフェイルオーバー
  • 稼働中のモデルデプロイ

このテストをすることにより、スケーリング時のダウンタイムが発見でき、Kubernetesの Pod Terminate時のベストプラクティス に従い、対処することが出来ました。

まとめ

アダプティブラーニングエンジンであるCALEの高いシステム品質を実現する取り組みを紹介しました。

100%安定したシステムを実現することは不可能にせよ、今回の記事で紹介した

  • 利用に則したテストを行う
  • SLI/SLOといった品質の基準をビジネスサイドおよび開発者全体で合意をとる

といった取り組みを続けていれば、システムが運用できるか、期待するシステム品質の実現できるかが自ずと分かり、高い品質のシステムの実現を達成できると考えております。

Classi では学校教育の現場で使われる高い品質のサービスを実現していく必要があります。 レコメンドエンジンを初めとした全国の学校で利用される新しい教育×データ活用サービス、そして、教育現場で安定稼働するシステムを一緒に作っていきたいと思った方は是非、Pythonエンジニアにご応募ください!

https://hrmos.co/pages/classi/jobs/0000003hrmos.co

個別学習機能の裏側を紹介: アダプティブラーニングエンジン「CALE」

みなさん、こんにちは。 データAI部でデータサイエンティストをしております廣田と申します。今回は、2022年5月にリリースした「全国模試に対応したAI搭載の個別学習機能」について、ユーザーからのフィードバック及びAIの部分について簡単に紹介していきたいと思います。

弊社ではこのAIを「CALE(Classi Adaptive Learning Engine)」と呼んでおり、本記事でもこちらの呼び方を使っていきたいと思います。

個別学習機能およびCALEについて

まず、個別学習機能について簡単に紹介します。こちらの画像を使いながら説明します。

個別学習機能の全体像(https://corp.classi.jp/news/2710/ より引用)

個別学習機能は

  • STEP 1: 先生によるテスト配信
  • STEP 2: 生徒によるテスト解答

の後のSTEP 3に対応する機能で、以下の流れを繰り返しながら生徒は学習していくことになります:

  • CALEが生徒の解答データを取得し分析
  • CALEが生徒におすすめの問題を複数選び、生徒に提示(推薦ロジックは後述)
  • 生徒は提示された問題の中から1つ問題を選び解答

複数の問題を提示する点が大きなポイントで、生徒の「どういう問題を解いていきたいか」といった意志を反映する余地を確保することで、生徒が納得感を持って取り組めるよう工夫しています。

CALEはこの個別学習機能の中で、生徒に合った問題を推薦する役割を担っています。

ユーザーからのフィードバック

幸いなことに、リリース以後多くの生徒にCALEを利用していただいております。ここでは、CALEを利用していただいた一部の学校の生徒に対して実施したアンケート結果について紹介いたします。

CALEが推薦した問題の難易度について尋ねたところ、一番多いのが「難しかった」で、次点で「ちょうど良かった」となりました。「難しかった」と「ちょうど良かった」には大きな差は無く、総評としては「やや難しい」と言えると思います。

CALEが推薦した問題の難易度についてのアンケート結果

CALEの問題推薦ロジックの基本アイデア

ここではCALEが問題を推薦する際の基本的なアイデアについて紹介します。ポイントは

  • ①問題の難易度調整
  • ②学習単元の遷移

の2点です。

①問題の難易度調整

基本的なアイデアは「難しすぎず易しすぎない問題を推薦する」というものです。易しすぎる問題では学べることも少ないですし、難しすぎる問題の場合はそもそも何かを学び取ることが難しいです。CALEでは「この生徒にこの問題を出した時、何%の確率で正解できそうか」を計算し、その予測正答確率の値に基づき、難しすぎず易しすぎないような問題を選び出しています。予測正答確率の算出には項目反応理論(Item Response Theory)を利用しております。

既に触れましたが、現状のCALEは生徒からは「やや難しい」と評価されております。仮に正答率が高めの問題を選ぶようにCALEの挙動を調整すれば、生徒が「ちょうど良い」と感じる割合が増えると予想されます。しかし単純に難易度を易しくしてしまうと、今度は生徒の学びに繋がる要素が減ることが予想されます。生徒の学習の進めやすさと学習効果、双方のバランスがちょうど良くなるラインを探ることは非常に興味深いテーマで、今も検討を重ねているところです。

②学習単元の遷移

上記のような難易度調整をしても全く問題に正解できないケースが存在します。例えば、現在学習中の単元の前提の単元の理解が不足しており、現状の単元の理解が進まないようなケースです。このようなケースの対応策として、CALEでは学習単元の遷移機能も備えております。

生徒が学習中の単元で全く正解できなかった場合、CALEはその前提知識に相当する単元にさかのぼって問題を選んでくるようになっております。もしさかのぼり先の単元の理解が十分だと判断されれば、元の単元から出題されるようになります。

ただし単元間の依存関係は自明なものではないため、「この単元ができなかった時にどの単元にさかのぼらせるべきか」といった点については今も検討しているところです。

今後の課題

課題は山積みです。例えば現状のCALEの問題推薦ロジックについての課題であれば

  • 出題難易度のバランス調整
  • さかのぼり先の単元の選択方法
  • 予測正答確率の精度向上

などが挙げられますし、さらに大きな枠組みで考えると

  • 現状のロジック以外の問題推薦ロジックの検討
  • 学習効果の測定

などが挙げられます。

学校の先生・生徒の声に耳を傾け、教育工学の知見も踏まえ、1つ1つ着実に解決していきたいと考えております!

さいごに

今回紹介したCALEをはじめとして、弊社ではAI系の機能の研究開発を積極的に行なっております。まだまだ課題が山積みなCALEを一緒に進化させていきたい、教育現場の課題を技術で解決したい、などの気持ちがある方はぜひこちらからご連絡ください!

Rubyの会社の手札にGoを加えるまで

こんにちは・こんばんは・おはようございます、System Platform領域所属のid:aerealです。

前回このブログに記事を書いた時の所属から変わり、チームトポロジーを参考に編成された新たな領域でチームを横断した関心事に取り組んでいます。

当社ClassiはRubyを主要な開発言語として採用し、各種Ruby関連のイベントに参加・協賛するなど「Rubyの会社」という印象を持たれているのではないでしょうか。

実際、今もClassiの屋台骨を支えているシステムはRubyで書かれていますし、今後1〜2年でそれが大きく覆ることは考えにくいでしょう。

そんなClassiに新たにGoという選択肢を有力なものとして加えるための過程と取り組みについて紹介します。

Classiの技術選定と手札たる要件

Classiの技術選定に対するスタンスと題したVPoTの丸山が書いた記事にあるように、この記事を執筆している現在は「この技術領域にはこれを使ってね」といった強い制約はありません。

一方で先の記事でもやる気のあるメンバーがめっちゃ新しい技術スタックでキラキラした開発をおこなったけど、それをメンテ、運用できる知見は誰ももっていないあるいはだれにも継承されず、あとには触るに触れないコンポーネントが残った……と触れられているように、闇雲に採用技術を発散させることを一概によしとできません。

このトピックに対し私なりに出した答えが社内コミュニティができて盛り上がっている状態になれば、技術選択の第一候補として置いてよいだろうというものです。

そもそもスキルや知識を持った開発者が存在しさえすれば良いのなら「新しい技術スタックが採用されたが、次第に腫れ物になってしまった」という出来事は起きないはずです。 たとえ提唱者が退職その他の理由で離れてしまったとしても、採用や育成をすればカバーできるはずです。

そこで私は「触るに触れないコンポーネント」ができあがってしまう力学に目を向けました。 そこからClassiの事例や自身の経験に照らして、なぜ触れない・触りたくないと思うかを考えると新しい技術に触れるリスクや恐怖が期待されるおもしろさやメリットを上回って見えるからだという仮説を導きました。

我々ソフトウェアエンジニアはプロダクト開発を通して顧客に価値を届けることが大前提で、他に求められる種々の資質などは細分化された結果のものです。

価値の中には提供速度や信頼性・安定稼働といった要素も当然含まれます。これらを満たすため、新しい技術を採用したコンポーネントを保守しつづけることを諦め、より廃止ないし・よりコンサバな技術セットで作り直すという判断に至るのでしょう。

またリスクや恐怖がおもしろさやメリットを上回って 見える ということも肝だと考えます。

人間は未知に対する恐怖心を抱えがちです。栄枯盛衰の激しい業界に身を置く我々Webエンジニアであっても多かれ少なかれそれは避けられないはずで、恐怖心は認知バイアスを呼びます。

認知バイアスがかかった状態では望ましい判断を選びにくくなってしまいます。

冷静かつ丁寧に事実を集め評価すれば悪くない選択肢だったとしても、負の側面が大きく見えるのなら「失敗だった」という判断になりえます。

またバイアスだけではなく感情的な要素も無視できないでしょう。「新しい技術を試行錯誤するおもしろさに立ち会えず、ただ尻拭いをさせられている」という意識は事実や理屈でそう覆せるものではありません。

これらのことから特定の誰かではなく社内でうっすらと盛り上がっている・ムーブメントが起きているという状況を作ることができたらもはやその新しい技術は未知のものではなく「自分はよく知らないけど社内ではよく聞くアレ」になります。

また、そういった状況を作ることができれば提唱者個人の負荷も減るでしょう。

取り組み

実際にGoの社内コミュニティを作るためにやってきた取り組みを紹介します。

Slackチャンネル

何はなくともまずSlackチャンネルです。5年以上前に作られたけど過疎ってarchiveされたチャンネルを掘り起こし #go-zassou として復活させました。

とはいえunarchiveしたからすべて良しとは当然なりませんので、まずは自分がGo関連のニュースや記事を紹介することから始めました。

おもしろそうなライブラリを紹介したり、カンファレンスの発表資料を紹介したり、内容は様々です。 とにかくチャンネルに人がいる感を高めるために「こういうところがおもしろそう」というコメントを添えて隔日〜週数回ペースを当初保ちました。

また社内でGoを採用したコンポーネントの開発に関する相談や質問は積極的に他のチャンネルからこちらに誘導しました。

たいへん地味でしたが、ここ数ヶ月では自分以外のメンバーが興味をもったニュースや記事を紹介してくれたり、自発的に質問してくれるようになったので、archiveされて存在がなかったころに比べたら盛り上がっていると言ってよいのではないでしょうか。

もくもく会、A Tour of Goをやる会

筆者以外はGo初学者であったり過去に趣味で触っていたけれど今はご無沙汰しているといったメンバーが多いです。

そんなメンバーのキャッチアップを盛り上げるためにA Tour of Goをやる会を不定期で開催しました。

それぞれ進度は異なるので画面共有して同時に進行したりはせず各々で進める、いわゆるもくもく会形式をとりました。

気軽に質問できる環境を提供することが目的で「そういう会があるなら」と参加してくれたメンバーも一定いたので成功したといえるのではないでしょうか。

ライブコーディング

ライブコーディングに参加することで、細かいエディタやツールの使い方からいわゆるGoらしい書き方・設計の勘所など情報量の多いインプットを得ることができるでしょう。

基礎知識を得た状態から次に実践的な開発に入る上でライブコーディングは広く満遍なくとっかかりを得るのに適しています。

またライブコーディングに参加できなかったメンバーにはGoogle Meetの録画機能を使って動画も共有しています。

野良レビュー

私個人が開発に関わっていないGoを採用したコンポーネントも登場してきたので、一通りのコードリーディングをしてコミットも見ています。

本番提供するサービスとしてより望ましい・より高い品質を狙える点はたくさんありますし、それらを一度にくまなく説明しきることは難しいので、ベストエフェートで発見次第、Issueを立てたりPull Requestを送っています。

カロリーの高い取り組みなので新しい言語や技術を導入する際にはこれくらいやるべきとは言えませんが、個人的にいろんなコードを読むのが好きな性質なので自分の楽しみになる上に実利も備えられてラッキーくらいの気持ちでやっています。

成果

  • 本番投入サービス開発経験: 1人→5人
  • 本番投入サービス: 1つ→3つ

数字でみるとこのような結果になりました。

また嬉しいことにサービス開発を通じてライブラリを作ってくれたメンバーも現れました。

https://github.com/minhquang4334/goapartment

マルチテナント構成のDBに対しテナントに応じた接続を提供するライブラリで、ちょうどマルチテナントDBを擁するClassiにとって今後必要になることが明らかだったので、そこにチャレンジしてくれたことと品質に驚き感謝しています。

今後

まだまだ精力的に盛り上げているのは私個人なので、私がClassiからいなくなってもGoの新規採用が続く状況になったとはいえないでしょう。

ただ、既にGoを採用したコンポーネントのメンテナンスに意欲を向けてくれる状況にはなっているのではないでしょうか。

実際にそういったシチュエーションを迎えていないので評価は難しいのですが、実際に使う・読み書きするというコンテキストでGoの話題を口にしているメンバーは増えたので良い方向に変わっていると信じています。

研修を終えて勉強になったこと

自己紹介

初めまして、エンジニアのmanhnguyen1998です。
2021年秋にベトナムからClassi株式会社に入社してから半年程新卒研修を受けました。
今回、研修で印象に残ったことについて振り返ります。

研修でやったこと

以下の内容の研修を受けました

日本語研修

仕事で最も重要なのはコミュニケーションです。意思伝達ができないと仕事もうまくできないと思います。そのため、研修で以下の内容を重視しました

日本文化の理解

  • 『クローズアップ日本事情15』という本を読みました。文化を学ぶことでコミュニケーションが取りやすくなると思います。また、文化を学んだ時は先生と一緒に相談し、説明能力も高められたと思いました

ビジネス知識

  • IT知識だけじゃなくて、ビジネスの知識も必要です
  • 色々な会社のリーダーのインタビュー記事を読んで日本の会社でどのようなことをやっているかを知り、考え方などを学びました

単語の学習

  • 仕事でよく使っている語彙がいっぱいあるので、毎日新しい語彙を勉強した方がいいと思います
  • esa、Slackで職場の語彙、表現を集め、これらがどのような場面で使われているのか学んだり、似ている語彙を探したりして学習しました

コミュニケーション以外の能力も高めたいと思いましたので、日本語能力試験(JLPT) N1レベルの学習もしました

  • 語彙
  • 文法
  • 模擬試験

Railsチュートリアル

ClassiのサービスはRailsを使っていますので、Railsチュートリアルで基本的な知識を勉強しました

  • Railsチュートリアルを進めました
    • 資料、動画を見ながらコーディングしました
  • 学んだ知識を言葉で説明するRailsチュートリアルトレーニングを受けました
    • YassLab社の安川さんのフィードバック会に参加しました
    • Railsチュートリアルやトレーニングの内容に関して理解を深めることができました。例えば、なぜRailsでStrongParameterを使うのかを聞いて、脆弱性を防ぐために使われていることを把握できて、自分の疑問も解消できました

PBL研修

  • チュートリアルの後、Railsチュートリアルのコードに機能追加するのを通じて、実際に自分で考えながら手を動かしてRailsのコードを書く練習をしました
    • Gitの使い方、Pull Requestの作り方を学びました
    • DB設計、Modelingを学びました
  • PBLの成果、学んだことは発表会を行って、プレゼンしました
    • 追加機能として成果があります。今回追加したのはリアクション機能です
      PBLでリアクション機能を作った

セキュリティ研修

  • ただコーディングするだけではなくて、セキュリティの脆弱性を防ぐ方法を学ぶことも大切なので、セキュリティ研修を受けました

Docker研修

  • Classiでは一部サービスがECS化されていることもあり、研修を受けました

フロントエンド研修

  • バックエンドと同じように、フロントエンドも大切で基本的な知識を勉強しました
    • HTML ・CSS・JavaScript・Angularの基本知識を学びました
    • 学んだ知識を生かしてフロントエンドコーディングをしました
  • フロントエンド研修が終わった後、発表会を行いました。プレゼンテーションしました
  • 今回、Angularを使ってFoodReviewのサイトを作りたいので、画面のレイアウトを変更しようと思いました
    FoodReviewのレイアウト

AWS研修

  • ClassiはAWSを使っていますので、そのサービスの使い方を勉強するのによい経験でした

OJT

  • 社内プロダクトのRails API実装タスクを担当しました
  • 実際に本番で使われているコードを通じて、手を動かし、どんな風に開発するかの流れを学びました
  • 長い時間だけど、いろいろなことを勉強できるし、卒業式で振り返ることができて嬉しかったです!

研修を通して学んだこと

目標は何か、そのために何をするべきかは大切

研修の目標は学習なので、タスクを達成、早く完了することではありません。タスクを早く完了できても、何も学習できなければ、その研修の目標は達成できません。そのため、早く完成させるのではなく、自分が何をどういった目的で学んでいるのかをずっと頭の中で考えながらタスクを進めました。例えば、コードを読んだ時、分からないところをちゃんと把握しメモを取っておくようにしました。時間はかかっていますが、メモした内容で、後で検索したり調べたりするようにして、疑問が解消できて勉強の目的が達成できました。今もこれからも、目標に基づいて計画を調整し、自分のやることを変更することは大切です。

目的は大事なこと

コミュニケーションも大切

ベトナム人として、言語も文化も、働き方や考え方も多少異なるところがあります。そのため、仕事の最中にミスをしたりわからないことが出てくることもあります。困ったらどうするべきか?このやり方が正しいのかをよく考えています。その時、コミュニケーションスキル、質問スキルはもっとも大切です。それらがあることでチームメンバーと情報共有することができるようになります。

コミュニケーションで基本的なことは

まとめるのも大切

研修で色々学びましたが、時間が経つと勉強した知識とやり方を忘れるかもしれません。だからこそ、何をやったか、何を学んだか、ちゃんとまとめて後で見返すのは良いと思います。esaを使ってメモをしていると、今後に役に立ちます。例えば、以前同じエラーを解決したことがあるものの、どうやったのかは忘れてしまったというときもあるでしょう。メモがあるとすぐに解決できると思います。また、自分も他の人もそのメモから学習し、問題が解決できるようになります。

どうやってメモするか

  • 具体的には、
    • esaで自己メモを書く
    • メモの書いた目的、問題、解決方法があると良いと思います
    • コード例、説明の画像があると分かりやすい
      esaにメモしたこと

最後に

入社してから半年程研修をやったおかげで、配属後もスムーズに業務に入ることができました。

例えば、チームでコミュニケーションができて作業の内容をちゃんと理解したり、RailsもAngularの知識もタスクをやる最中に活用できたりしました。まだ全部の研修で勉強した内容は使えていませんが、これから使う機会がありそうですし、そのときは役に立つと思います。

研修できたのは皆さんのおかげで、カリキュラムもサポートも受けて学習できました。 これからも良いプロダクトを作って貢献していきます!ありがとうございました!

© 2020 Classi Corp.