東京大学講師の松井と申します。本記事は壁 Advent Calendar 2021の12/15の記事です。英語版はここです。

はじめに

本記事では、GitHub Classroom + GitHub Actions + CML Containerを使い、GitHub上に構築する簡単自動採点システムを紹介します。本システムにより以下が可能になります。

  1. 採点者はプログラミング課題を受講者に課す。
  2. 受講者は課題を解く(コードを書く)。
  3. 受講者は書いたコードを何度でも採点システムに提出できる。システムはその度に採点結果を自動的に受講者に返す。
  4. (最終提出コードに対して、あらためて採点者が別のパラメータで本番の採点を行う)

プログラミング教育において、採点のフィードバックは非常に重要です。 手を動かし試行錯誤することがプログラミング上達のために必須であることは言うまでもありません。 自動採点による採点フィードバックは、そのような試行錯誤の機会を受講者に与えることが出来ます。 また、「自動採点をパスする」という行為を通じて、スペースの入れ忘れといった出力フォーマットのケアレスミスを防ぐ効果もあります。

本記事では受講者は初めて大学でC言語を学ぶような初学者であると仮定します。受講者に必要な手続きはGitHubアカウントを準備することだけです。

本記事の読者としては大学等でプログラミング講義を持っておられる先生を対象としています。コメント・フィードバック歓迎です。

自動採点システム

それではシステムを紹介します。全てのコードはここから確認できます。 ここでは課題はGitHubのリポジトリという形をとっており、以下のような構成になっています。

.
├── .github
│   └── workflows
│       └── autograding.yaml
├── README.md
├── eval.py
└── main.c

このリポジトリを各受講者に一人一つずつ配り、main.cを自由に編集してもらいます。 今回の課題は、$ ./a.outとしたときにHello World!と表示すること、としましょう。

main.cの中身には以下のような雛形が書かれています。

#include <stdio.h>

int main () {
    printf("Hello Word!!\n");
}

ここではprintfの中身に間違いがあるので、それを修正することが課題となります。 受講者は自分の環境にこのmain.cをコピーし、コーディングを行います。 コーディングが終ったら、受講者はGitHub上のmain.cに結果をコピペすることで、リポジトリを更新します。 ここでは受講者は初学者を仮定しているので、リポジトリを更新する際も、gitコマンドを使うのではなく単にGitHub上から ファイルを編集してもらってOKとします。もちろんgitコマンドを使ってもOKです。

これで終わりです。main.cを編集したあと、自動採点結果がコミットにぶら下がる形で直接表示されます。確認してみましょう。 ここでは受講者がHello Word!!Hello World!!に書き換えたとします。 ここで、受講者はリポジトリのトップ画面から、右上の「コミット一覧」を押します。

すると次のようにコミット一覧画面が出ますので、最後に行ったコミットを選択します。

ここで、コミットに対応する更新内容が、次に示すように表示されます。 しかし、ここでは実は!が一個多いままなので、失敗してしまいます。 その内容が、コミットの下部にgithub-actionsというボットからの通知という形で 表示されています。実際のコミット画面はこちら

ここで受講者は再度修正を施し、Hello World!と正しく表示するようにします。そうすると、次のように、自動採点が成功となります。 実際のコミット画面はこちら

このようにして、受講者は、main.cを更新する度にインタラクティブに自動採点の結果を知ることができます。 そして、自動採点結果は、上記のようにGitHubのウェブインタフェース上から数度クリックするだけで簡単に確認することができます。

仕組み

なぜコミットするだけで自動採点が実行できるか説明します。 eval.pyは、main.cをコンパイル・実行し、結果をチェックするコードになっています。 なので、採点そのものはeval.pyを実行するだけで完了します(解説のためにeval.pyはシンプルなコードにしていますが、ここはいくらでも工夫が出来ます)

import subprocess

def run_job(cmd):
    ret = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return ret.stdout.decode(('UTF-8'))
    
if __name__ == '__main__':
    # Compile
    run_job("gcc main.c")

    # Run
    msg = run_job("./a.out").rstrip()
    if msg == "Hello World!":
        print("Ok!")
    else:
        print("Error. The correct answer is `Hello World!`, but your outupt is:")
        print("```")
        print(msg)
        print("```")

さて、このeval.pyを実行すればいいわけですが、それを受講者に手動でやってもらうわけにはいきません。 なぜなら、

  • 受講者は初心者なので、スクリプトをダウンロードし実行するという手間をとらせたくありません。あくまで、「main.cの更新時にシステムが自動的に採点を実行」という形にしたいです。
  • 受講者PCでスクリプトを実行してもらおうとすると、python環境を受講者側で準備する必要があり、手間です。

そこで、GitHub Actionsをもちいて自動的に上記のスクリプトをクラウド環境で実行します。 その手順は.github/workflows/autograding.yamlに記述してあります。 これにより、受講者のコミットに反応してeval.pyが実行され、それがコミット画面に表示されるようになります。

name: autograding
on: push
jobs:
  run:
    runs-on: ubuntu-latest
    container: docker://ghcr.io/iterative/cml:0-dvc2-base1
    steps:
      - uses: actions/checkout@v2
      - name: autograde_task
        env:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
        run: |
          python eval.py >> report.md
          cml-send-comment report.md

いくつかのポイントを見ていきましょう。

  • ここではon: pushとあるので、コミットがpushされるとjobs以下が自動的にクラウド計算資源上で実行されます。
  • ここで重要な点は、container: docker://ghcr.io/iterative/cml:0-dvc2-base1によりContinuous Machine Learning (CML)のdockerイメージを用いていることです。これにより、最後の行のcml-send-commentというコマンドが実行可能になります。
  • stepsの中では、評価コードであるpython eval.pyを実行し、その結果をreport.mdとして吐き出しています。
  • そのマークダウンをcml-send-commentによって、コミット画面に張り付けます。

CMLはもともと、CI/CDを用いた機械学習を実現するための仕組みです。cml-send-commentは、もともと、actions中で実行した計算結果をPull Request画面に表示するというコマンドです。本システムでは、その機能を、自動採点のフィードバックのために利用しています。 あとは、eval.pymain.cを課題に応じて準備すればおしまいです。

本方式のメリットは以下になります。

  • 基本的に無料です(後述するように、自動採点の量が多いと無料では実現できなくなります)
  • 採点プロセスをGitHub Actionsに担わせることで、自分が中央集権的な採点サーバを維持する必要がありません。
  • 準備するコードとしては上で全てなので、採点者側の準備の手間は少ないです。また、特殊なサービスを使わないので、採点者が全てのプロセスをコントロールできます。
  • 受講者から質問がきた場合なども、全ての作業ログがリポジトリ上に保存されているので、原因の検証が簡単です。
  • 受講者側からすると、リポジトリの中身を書き換えると自動採点が走るので、「提出忘れ」や「提出エラー」が無く、直感的です。

Q&A

以下に、想定されるQ&Aを述べます。

  • どうやってリポジトリを配るのか?
    • 「リポジトリを配る」のは結構大変です。(1) 課題の雛形リポジトリのコピーをプライベートリポジトリとして受講者に配る、(2) 受講者同士はお互いのリポジトリが見えないようにする、(3) 一方で採点者は全てのリポジトリを確認できるようにする、という条件をクリアする必要があります。実はここではまさにその機能を提供するGitHub Classroomというものがあり、それを直接使うことで解決します。
    • 少人数講義の場合は手動でリポジトリを作成し権限を設定するといった方式でも問題ないと思います。
  • 本番採点はどうするのか?
    • GitHub Classroomにおいて作成したリポジトリはGitHub Classroom Assistantというソフトウェアを用いて全てローカルに簡単にダウンロードできます。ローカル上で別パラメータでeval.pyを走らせることで、本番採点を行うことが出来ます。
    • たとえば、引数の数の和を計算するプログラムを考えます。自動採点では./a.out 3 5に対し8を出力するかどうかをチェックします。この場合、printf("8");のように直接8を出力するズルのプログラムを書くと自動採点はパスできてしまいます。しかし本番採点では./a.out 10 7に対し17になるかチェックすることで、ズルのプログラムをはじき、ちゃんと採点することが出来ます。
  • 他サービスとの比較
    • マネージドサービス(Gradescope, Techful, etc)
      • 全てをマネージしてくれる別サービスを使う場合、しっかりと使うことができれば採点者側は管理する必要がないので楽だと思います。受講者側からしても、綺麗なUIでわかりやすいと思います。弱点としては、最初に別サービスのアカウントを作ってもらう必要があるなど初期コストが高いということ、有料の可能性が高いということ、ちょっと自分独自のことをしようとすると大変であるということ、が挙げられます。
    • GitHub ClassroomのAutograding
      • 実はGitHub ClassroomにはAutogradingの機能があります。しかし、採点結果の確認画面がActionsタブの奥深くのCI/CD結果チェックの部分になってしまいます。なので、初心者にとってはそこを見るということは結構なコストになります。よって、今回はcommitにぶら下げるという形式を作りました。中級者以上を対象とした講義では、このGitHub Classroom Autograding機能が楽でよいと思います。
      • 【参考 VSCodeと組み合わせた例】:GitHub Classroomでの課題提出, 応用計量分析2 (2021), 梶野洸:GitHub ClassroomにはVSCodeと連携し課題を実行する機能があります。それをうまく使うと非常に簡単に自動採点を実現できます。Pythonを用いる場合はこのやり方が分かりやすいと思います。
    • 手製のサービス
      • よくあるのは、(1) メールあるいは大学のLMSサービスを使って課題を提出してもらう (2) 採点プログラムを立ち上げておき、提出された課題を解析して採点する。というものです。この方式の弱点は(i) 自分でパイプラインや採点プログラムを構築し維持する必要がある。(ii) 学生にフィードバックを返すのが難しい。(iii) 学生側からすると、メール添付したりLMSサービスに提出するのはちょっと手間。ということが挙げられます。提案システムは、このような手製サービスをGitHubを用いて簡単に実現出来ないか、という着眼点のもとに作りました。
    • 競技プログラミングのジャッジメントシステム(Aizu Online Judge, oj, etc)
      • 競技プログラミングにおけるオンラインジャッジシステムは、本記事と目指すところが似ています。それらのオープンソースの実装も存在します。このあたりは調査しきれていないので、うまく組み込めるのかもしれません。
  • 自動採点に1分ぐらいかかって遅い
    • 現在、CML imageを毎回ダウンロードする必要があるため、採点に毎回1分ぐらいかかります。これはインタラクティブというにはちょっと遅いです。また、後述するactions無料枠の消費という点からも遅いのは好ましくありません。
    • これを解決するためにはactions/cacheを用いるか、あるいはself-hosted runnerを用いるとよいです。self-hosted runnerとは、GitHubのサーバではなく、自分で一台PCを用意して、そこでactionsを走らせるというものです。これを用いると、containerのpull部分は一度行えば次からはそれを再利用してくれるので、pullにかかる1分がなくなり、全体で20秒程度で終わるようになります。「インフラが必要ない」というのが本システムの推しポイントだったので、ちょっと矛盾ではあるのですが。。。(ちなみに、github側がcontainerのpullのcache機能をデフォルトで導入してくれると勝手に解決するので、是非実装してほしいところです。)
  • actionsの無料枠を使い切ってしまうのでは?
    • その通りです。actionsの無料枠は決まっているので、それを使い切ると採点できなくなります(有料になります)。その場合、再度ですが、self-hosted runnerを使うと良いです。self-hosted runnerだと、いくら使っても無料です。
  • このシステムを自分の講義で使ってもよいか?
    • もちろんOKです!雛形コードはMITライセンスとしましたので、是非使ってください。また、もし講義等で使用していただいた場合、松井まで一報いただけると嬉しいです(このブログに追記します)