AtCoderヒューリスティックコンテストの、ローカルテストをサポートするツールです。
前作 【競プロ】テスト10倍速!AtCoder AHC向け【Python】自動テスト並列処理ツール の以下の特徴を継承しています。
- ローカルテストための初期環境整備(
steup.py
として独立) - テスト対象プログラムの更新を検知して自動再コンパイル
- 簡単なコマンドラインオプションによる動作制御(条件によっては、一部、ソース修正が必要)
- 大量のテストを素早く完了できる並列処理
今作では、さらに、以下の特徴を追加しています。
- 初期環境整備を
setup.py
として別プログラム化し、本体をシンプルに - 初期環境整備で
.gitignore
やrust-toolchain
も作成 - インタラクティブ型、非インタラクティブ型を自動切り替え
- コンパイル後にダミー実行するようにして、コンパイル直後の実行が遅いことを防止
- Pythonの並列処理ライブラリ
ray
を利用することで並列処理を安定化 - エラーやTLEでテスト対象プログラムが落ちた時、プロセスが暴走することがあったバグを解消
- テストケースの特徴量によるスコア差がわかりやすくなるように、特徴量をスコアとあわせて表示
- 相対スコアゲーに効果ある
$Σ\log(1+score)$ 評価を追加 - Optunaに対応、ストレージ、enqueue_trial、枝刈りにも対応
- https://img.atcoder.jp/ahc_standings/index.html (以下、ahc_standingsと呼びます)のローカル実行に対応
適切なPython3.11
とRust1.70.0
の実行環境を、準備しておきます。
ディレクトリ構造は、cargo_comete
で作成されるものを前提としており、以下に例示します。この通りでなくても、ソースを修正することで対応可能です。
コンテストルートに、setup.py
とeval.py
をコピーするとともに、ローカルテストツールをダウンロード、解凍して配置してください。以降はすべてコンテストルートで作業します。
ParentDir
├── ahc028
├── ahc029 (コンテストルート(例))
│ ├── setup.py (コピーする)
│ ├── eval.py (コピーする)
│ ├── src
│ │ └── bin
│ │ └── a.rs (Rustの提出ソース)
│ └── tools (ローカルテストツール)
│ ├── src
│ │ └── テストツールのソース群
│ └── in
│ └── テストケース入力ファイル群
└── target
├── debug
└── release
└── Rustの実行ファイル群
python setup.py
を実行します。
- 以下のファイル・ディレクトリを作成してくれます。
./.gitignore
./rust-toolchain
tools/out
-
自動でahc_standingsから
index.html
をダウンロードしてtools/out
に配置してくれます。 -
運営から提供されたローカルテストツールを、自動でコンパイルしてくれます。
提出プログラムを作成します。(これは解説できませんww)
eval.py
のソース冒頭にある以下を、条件にあわせて変更します。なお、FEATURES
は、変更しなくても一部表示が正しくなくなるだけで、動作はします。しかしながら、ahc_standingsを使う上でも必要ですので、ちゃんと変更しましょう。
# 条件にあわせて以下のみ変更する
LANGUAGE = 'Rust' # 'Python' or 'Rust'
FEATURES = ['N', 'M', 'K'] # 特徴量
代表的な最適化例を記載します。オプションパラメータの詳細は--help
オプションで出るヘルプを参照ください。
以下にて、単一のテストを実行してデバッグ出力を確認することができます。
python eval.py -s 0 -v
提出プログラムがバグ無く実行完了して、予定した動作ができているか、実行時間は適切か、など細かく確認したい場合に使います。予め、提出プログラムに、デバッグ出力を入れておきます。
なお、以下で複数テストケースを実行し、得点や実行時間の変化を見ることで、さまざまなテストケースにおいて、期待通りの動作をしているかを、確認することが可能です。
python eval.py -s 0 49
動作が変なテストケースにしぼって、単一テストでデバッグ付きの実行をすることで、詳細確認します。
処理前と処理後で、以下にて、テスト0-49を実行します(テストケースは場合によって増やします)。
python eval.py -s 0 49
スコア合計や、logスコア合計について、プログラム処理変更前後の差異を確認し、プログラムの改善度合いを確認します。
さらに、ahc_standingsによって、ダッシュボードを使って、試行全体の分析をすることが可能です。以下のコマンドでahc_standingsをローカルサーバとして起動できます。
python -m http.server --directory tools/out 8000
起動後に、以下のURLで確認が可能です。contes=0_49
は実行したケースにあわせて修正します。
http://127.0.0.1:8000/index.html?contest=0_49
Optunaでハイパーパラメータを最適化することができます。
事前に、ソースの以下の箇所を修正して、Optunaで最適化するハイパーパラメータをセットします。ハイパーパラメータは環境変数として実行プログラムに流し込まれますので、予め、実行プログラム側で環境変数を読み取って、ハイパーパラメータをセットするように作り込んでおく必要があります。
# 環境変数で流し込むパラメータ
# int: suugest_intの係数、float: suggest_floatの係数(3番目はstep, 4番目はlog)
# log: Trueの場合、setpは無視される
# enque: enque_trialの値(複数あれば複数回実行)
DIRECTION = 'maximize' # 'maximize' or 'minimize'
PARAMS = {
'AHC_PARAMS_SAMPLE1': {'int': [0, 1000], 'enque': [500]},
'AHC_PARAMS_SAMPLE2': {'float': [0.0, 1.0], 'enque': [0.5]},
}
- 'int'キーの後ろの値は、
sugggest_int
のパラメータになります。最初の2つが最小値と最大値です。 - 'float'キーの後ろの値は、
sugggest_float
のパラメータになります。最初の2つが最小値と最大値です。 - 'enque'キーの後ろの値は、
enque_trial
のパラメータになります。複数ある場合は、enque_trial
が複数回実行されます。
enque_trial
とは、Optunaの試行において、固定値をセットするメソッドです。過去の最適値をセットすることで、新たな試行において最適値の周辺の探索機会が増え、さらなる最適値を見つけやすくなります。
DIRECTIONの設定を変えることで、最小値が良いのか、最大値が良いのかを、切り替えることがてきます。
実行は以下にようにして、-o
オプションにOptunaの試行回数を設定します。テスト0-999の実行を1試行として、Optunaで1000回試行する例です。
python eval.py -s 0 999 -o 1000
実行にあたっては、自動的に枝刈り(prune)を行い、無駄な探索はしないように設定しています。
pruneとは、大量のテストケースセットで試行する場合に、試行途中で非常に悪い成績のものは途中で試行を中止することで、より短時間に効率的な試行を行う機能です。
試行全体の結果は、Optunaストレージに保存されていますので、optuna-dashboardによって分析することが可能です。以下のコマンドでoptuna-dashboardが起動します。
optuna-dashboard sqlite:///tools/out/optuna.db
// 環境変数が無かった時のデフォルト値(=最終提出するベストな値を入れる)
const P1: usize = 5;
const P2: f64 = 0.5;
fn main() {
// 環境変数の取得
let p1: usize = os_env::get::<usize>("p1").unwrap_or(P1);
let p2: f64 = os_env::get::<f64>("p2").unwrap_or(P2);
println!("p1: {p1}, p2: {p2}");
}
// 他の環境変数とのバッティングを避けるため、プレフィックスをつけ、
// AHC_PARAMS_P1, AHC_PARAMS_P2が環境変数となる
pub mod os_env {
const PREFIX: &str = "AHC_PARAMS_";
pub fn get<T: std::str::FromStr>(name: &str) -> Option<T> {
let name = format!("{}{}", PREFIX, name.to_uppercase());
std::env::var(name).ok()?.parse().ok()
}
}