【解法解説】日本人工知能オリンピックで金賞(5位)受賞したよ!JOAI2025参加体験記!

  • URLをコピーしました!

お久しぶりです~。ちょっと大学とかが忙しくてあんまブログ更新できてませんでした。

突然ですが、実はこの前第一回となる日本人工知能オリンピック(JOAI)2025に参加していて、なんとオープン枠5位になって金賞を受賞することができました🥇

というわけで、今回はJOAIの体験談や感想だったり、提出したソースコードの解説とかをしてみようと思います。

スポンサーリンク
目次

JOAIってなに?

そもそもJOAIって何?っていう人もいると思うので、簡単に概要だけ説明。

日本人工知能オリンピック(JOAI)は、人工知能(AI)の知識とスキルを競う日本初のコンペティションで、2025年に第1回が開催されました!この大会は、国際科学オリンピックの一つである「国際人工知能オリンピック(IOAI)」の日本代表選手を選ぶ場でもあります。

2025年8月に中国・北京で開催されるIOAI2025への切符をかけて選抜枠の参加者がしのぎを削る一方、オープン枠では年齢や学歴、国籍を問わず誰でも参加できるのが特徴です。

Ray

私は大学生なのでオープン枠で参加しました。

JOAI2025は、4月25日から5月2日までオンラインで開催され、Kaggleというプラットフォームを使ってAIに関する課題に挑戦する形式でした。参加者は、与えられたデータセットを使ってAIモデルを構築したり、アルゴリズムを工夫したりして、問題解決を目指します。

受験案内を見ると、受賞ラインはこんな感じ。努力賞は誰でももらえるのが嬉しいですね。

入賞区分入賞条件
🥇 金賞上位 10%
🥈 銀賞上位 20%
🥉 銅賞上位 40%
🟢 努力賞サンプルベースラインを超える

Discordで他の参加者と交流できたり、開会式のアーカイブ動画でルールをしっかり確認できたり、チュートリアルコードが用意されていたりと、初心者にも優しい環境が整ってるのが嬉しいポイントでした!

JOAIに参加しようと思った理由

JOAIの存在を知ったのは、Twitterでたまたま流れてきたツイートがきっかけでした。

なんか突然これ👇が出てきて、思わずリンクをクリック。なんか難しそうだったけど、Kaggle形式のAIコンペティションって書いてあったので興味が湧いてきました。

深層学習の知識はほぼゼロだったんですが、Kaggleならちょっとだけ経験があって。
前にタイタニック生存者予測のコンペを試したことがあったんですよね。それが楽しかった記憶があって、「これならいけるかも?」って思いました。

それに、JOAIはオンラインで参加できるし、オープン枠があったのも大きかったです。気軽に参加できるし、負けても別に死ぬわけじゃないし、いつものように課題のスケジュールとかを考えずにノリと勢いで申し込みをしました。

Ray

言語学オリンピックもオープン枠があるよ。

参加前の実力とか

お前誰だよっていう人もいると思うので、簡単に自己紹介をします。

私は普段はマレーシアのモナッシュ大学というところで海外大生をしている、情報学部の1年生です。学部1年の半年が終わったところなので、特に専門知識がそこまであるわけでもありません。
普段は留学ブログを書いたり、海外旅行をしたり、言語学オリンピックとかの課外活動をしたり、像使いになったりしてます。気になったらプロフィールを見てほしいです。

コンペの参加に関しては、一応Pythonの文法はちょっと分かっているのと、機械学習のことだけでいうと東大松尾研のGCIをちょっと前に修了したくらいの実力でした。
ただ、今回のコンペで用いたようなディープラーニングについては全く知識がありませんでした。大会1週間で勉強を頑張ったら入賞できたので、自分でもビックリです。

本当の初心者なので、平然と間違ったことを書いているかもしれません。あくまで参考程度にお願いします。

コンペの内容

画像・表・テキストの3つの形態の情報をもとにして、ガスの分類を行う予測モデル作ろう、というコンテストでした。

このコンペティションでは、表・画像・テキストのマルチモーダル情報から、ガスに関する分類タスクに取り組んでいただきます。

各データは、ガスセンサによる2つの測定値と、同時に赤外線カメラで撮影された画像、および画像を説明したテキストから成ります。

ガスは「Perfume」「Smoke」の2種類存在し、それぞれの有無を考慮した4つのラベルが存在します。(Perfume, Smoke, Mixed, NoGas)

データの概要はこう。

  • MQ8 – ガスセンサによる測定値。
  • MQ5 – ガスセンサによる測定値。
  • Caption – 画像を説明したテキスト。本コンペティションで提供している画像は一部を切り抜いているが、このテキストは切り抜き前の画像を基に生成した。
  • image_path_uuid – images フォルダ内の対応する画像。
  • Gas – 正解ラベル。

ちなみに、使用できるモデルは制限されていて、このようになっていました。

種別モデル名呼び出しリンク
画像resnet50.a1h_in1ktimmで指定可能https://huggingface.co/timm/resnet50.a1h_in1k
言語FacebookAI/roberta-basetransformersのAutoModelで指定可能https://huggingface.co/FacebookAI/roberta-base
言語microsoft/deberta-basetransformersのAutoModelで指定可能https://huggingface.co/microsoft/deberta-base
マルチモーダルopenai/clip-vit-base-patch32transformersのCLIPModelで指定可能https://huggingface.co/openai/clip-vit-base-patch32

工夫したこと・解法の要点まとめ

長くなるので要点だけ先にまとめます。生成したモデルの基本データはこんな感じ。

  • 使用モデル(言語):microsoft/deberta-base
  • 使用モデル(画像):resnet50.a1h_in1k
  • センサデータはMLPで処理
  • Public:0.98431
  • Private:0.98128
  • 結果:オープン枠5位/全体10位(金賞受賞)

基本的にはチュートリアルコードをベースに改良したモデルを作りました。

具体的には、コードの内容を一度読んで全部把握して、ところどころ改善できそうなところを(ルールの範囲内で)ChatGPTを活用してアドバイスをもらいながら手動で修正して、精度をテストして…という作業を繰り返しました。

チュートリアルでは画像のみでの学習となっていたので、画像+キャプション+パラメータを活用した3モーダルモデルを採用することにしました。
言語の使用モデルはFacebookとMicrosoftで迷ったんですが、最終的に一番性能が良かったMicrosoftのものを採用しました。

データは基本そのまま使っていたんですが、主な変更点としては画像には軽めのオーグメンテーションを追加して、ハイパーパラメーターを手動で調節して、フォールド数を良い感じに回したモデルを平均アンサンブルして、という感じです。

計画性がない人間なのでコンペの4日前になってから全作業をやりはじめました。当然時間がなかったので学習回数は少なめにして、全ての実行は1~2時間で終わるようにしました。
そんなことをしていたら、最終日に良い感じにハイパラ調節が上手くいって急激にスコアが上昇し、入賞することができました。

といいつつ、最後はかなりの人がPublicとPrivateで順位が大きく異なっていたようなので、過学習対策が成功のカギだったコンペなのかもしれません。私は30位ほど最終順位が上がりました。
50%のデータがPrivateとのことで私はリーダーボードの順位よりも汎用性を重視したモデルを意識して作ったので、この作戦が結果として功を奏しました。

スポンサーリンク

解法の解説とか

EDAと特徴量の分析

公式様からいくつかコードをお借りしています。

データの形状は

  • trainデータ: (5760, 5)
  • testデータ: (640, 4)

となっていました。特徴量も非常に少ないです。

  • センサ値2種
  • 赤外線画像
  • 説明文(テキスト)

データセットも不均衡データではないみたい。欠損値も確認しましたが、ありませんでした。

MQ8とMQ5という数値データについてはなんか偏りがありそう。

画像とキャプションはこのような形式らしい。

ここで、

  • 特徴量が非常に少ない
  • データも特に不均衡でもないし、欠損値もない

という特徴から、ある程度特徴量エンジニアリングを頑張ってみて、それでもダメだったら純粋なモデルの作り方の精度だけで競い合うことになりそうだな、という印象を持っていました。このカンは当たっていて、実際にそうなりました。

モデル構築の試行錯誤

次に、モデル構築の際に考えていたことを書いていきます。

モデル選定:機械学習の導入?

チュートリアルコードでは画像だけを使っていたにも関わらず、F値0.89〜0.90くらいが出ていた記憶があります。そのため、かなり上位で競り合うコンペになりそうだなと思い、精神的にこの段階では萎えていました。

ただ、色々データがあるんだから数値データやキャプションデータも使うに決まってるよな~というのは初心者ながらに考え付いたので、どうやって画像以外のデータを学習に取り入れるかを考えました。

ここで私は初心者なので、「これ進○ゼミでやったところだ!」と思って、とりあえずGCIで愛用していたLightGBMを用いて数値データで予想をすればいいんじゃないかと思いましたが、色々調べていくうちにどうもこういうものに対してはMLPというものを使うらしいことが分かりました。こういうのを馬鹿の一つ覚えっていうんですね(結局使ってないけど)。

あとは、「【重要】利用可能な事前学習済みモデル許可リスト」という運営からのお知らせを目にしました。

そこにはこんな表が。

種別モデル名呼び出しリンク
画像resnet50.a1h_in1ktimmで指定可能https://huggingface.co/timm/resnet50.a1h_in1k
言語FacebookAI/roberta-basetransformersのAutoModelで指定可能https://huggingface.co/FacebookAI/roberta-base
言語microsoft/deberta-basetransformersのAutoModelで指定可能https://huggingface.co/microsoft/deberta-base
マルチモーダルopenai/clip-vit-base-patch32transformersのCLIPModelで指定可能https://huggingface.co/openai/clip-vit-base-patch32

初心者にはちょっと何言ってるか分からない☆ サンドウィッチマンもビックリ。

ChatGPT君に相談してみると、要するにこれを使って予想をしろということらしいです。よく考えたらこれは日本語能力の問題でした。

モデル選定:マルチモーダルモデルの導入

というわけで、画像はチュートリアルのresnet50.a1h_in1kしか選択肢がないのでいいかと思いましたが、それ以外のものをどうするか問題。センサデータはMLPで処理するとして。

てか、まずマルチモーダルってなんだよ。この前大学で怪しい勧誘をしている先輩がいたので、その人に「マルチ」っていうあだ名をつけてたんですが、その人のことをふと思い出しました。

調べてみると、マルチモーダルというのは、異なる種類の情報をまとめて扱うAIのことらしいです。なるほど、じゃあこれを使えばいいのか。これもよく考えたらプログラミングじゃなくて英語の問題でした。

と思ってOpenAIのモデルを使ってみたら…

おめでとうございます!スコアが大幅に下がりました🎉

モデル選定:言語モデルの導入

というわけで、こりゃイカンと思って、とりあえず最初はmicrosoft/deberta-baseに切り替えました。そしたらスコアがチュートリアルよりも何%か上がった記憶があります。

Facabookのモデルも使ってみて同じくらいの性能は出たんですが、

  • MicrosoftのほうがFacebookの改良版らしいとの情報があった
  • 実験してみて若干精度が良かった

ためにMicrosoftを採用することにしました。最近インスタのアカウントを利用停止したので、もしかしたらFacebook(Meta)から嫌われてしまって低いスコアが出たのかもしれません。

特徴量エンジニアリング

まず、GCIの知識を用いて特徴量エンジニアリングを行うことにしました。

MQ5と8を反転させて足し合わせたり、かけ合わせたりしてみたが、これは上手くいかず。

キャプションには温度などが含まれていたので、℃や°Fの記号を使って温度だけを抜き出して機械学習の特徴量に加えたりしましたが、これも上手くいかず。

そのため、大胆な特徴量エンジニアリングは諦めて、基本は値を全部そのまま使うことにしました。

最終モデルの枠組み決定

こうしてできたモデルの枠組みがこちら。

  • 3モーダル構成:センサ値+画像+キャプション
    • 使用モデル(言語):microsoft/deberta-base
    • 使用モデル(画像):resnet50.a1h_in1k
    • センサデータはMLPで処理
  • 数値データは標準化だけする
  • キャプションはトークナイズしてモデルに入力
  • 画像ほぼチュートリアルのまま
    • ただし、汎用性のあるモデルにするために軽めのオーグメンテーションを加えた
    • 画像をランダムに切り取ったり、反転させたり、色調を変えたり
    • これによってちょっとだけスコアが上がった

詳しい構築はコードを見てください。

# Datasetクラスの定義とデータオーグメンテーション
from torchvision import transforms

class TrainGasDataset(Dataset):
    def __init__(self, cfg: Config, df: pd.DataFrame, tokenizer):
        self.cfg = cfg
        self.df = df
        self.tokenizer = tokenizer

        # 画像用のtransform(データオーグメンテーションを追加)
        self.transform = transforms.Compose([
            # ランダムに画像を切り取る(大きさを224x224に変更)
            transforms.RandomResizedCrop(size=224, scale=(0.95, 1.0)),  # 少し緩やかにしてみた
            # ランダムに画像を反転
            transforms.RandomHorizontalFlip(p=0.5),
            # ランダムに画像を回転(角度を小さく)
            # transforms.RandomRotation(degrees=8), # ここも緩やかに。提出時は入れていないが、入れるのもアリ。
            # 色調をランダムに変更(明るさ、コントラスト、色、シャープネスなど)
            transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15, hue=0.05), # 範囲を縮小
            # 画像をテンソルに変換
            transforms.ToTensor(),
            # 汎用性能を増加
            transforms.RandomErasing(p=0.1, scale=(0.01, 0.05)), #追加した方がよさそう
            # 画像を正規化(平均値と標準偏差を設定)
            transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225]),
        ])


        # センサーデータ用の変換(手動標準化)
        self.sensor_mean = [0.0, 0.0]  # 例: MQ8とMQ5の平均
        self.sensor_std = [1.0, 1.0]   # 例: 標準偏差(必要に応じて調整)

    def __len__(self):
        return len(self.df)

    def __getitem__(self, index):
        row = self.df.iloc[index]

        # 画像読み込みと変換
        img_path = self.cfg.env.image_dir / row["image_path_uuid"]
        image = Image.open(img_path).convert("RGB")
        image = self.transform(image)

        # センサデータ(MQ8, MQ5)の取得と手動標準化
        sensor = torch.tensor([row["MQ8"], row["MQ5"]], dtype=torch.float32)
        sensor = (sensor - torch.tensor(self.sensor_mean)) / torch.tensor(self.sensor_std)  # 標準化

        # キャプション → トークナイズ(DeBERTaなどの入力形式に変換)
        caption = row["Caption"]
        tokenized = self.tokenizer(
            caption,
            padding="max_length",
            truncation=True,
            max_length=64,
            return_tensors="pt"
        )
        input_ids = tokenized["input_ids"].squeeze(0)  # (seq_len,)
        attention_mask = tokenized["attention_mask"].squeeze(0)

        # ラベルの取得
        label = NAME2LABEL[row[TARGET_COL]]

        return {
            "image": image,
            "sensor": sensor,
            "input_ids": input_ids,
            "attention_mask": attention_mask,
            "label": label
        }


from transformers import AutoTokenizer

# DeBERTaのトークナイザーを作成
tokenizer = AutoTokenizer.from_pretrained(cfg.exp.text_model_name)

# tokenizer を渡して Dataset を作成
train_dataset = TrainGasDataset(cfg, train_df, tokenizer)

ハイパーパラメータの調整

この時点でコンペ終了の2日前とかになってたのでガチ焦りです。

この段階では銅賞(上位40%)にかすってもいなくてほぼ最下位レベルだったのですが、かといって他に試せることのアイデアもなかったので、あとはいかにハイパラ調節ができるかにかかっています。

ハイパーパラメーターのチューニングはoputunaやベイズ最適化などを使っても良かったんですが、以前使い方がよく分からなくて苦労したのと、普通に時間がなかったので、手動で大まかに調節することにしました。

まずはChatGPTに大まかな値を教えてもらい、それを一つずつ変えてみて結果を見る、という脳筋作戦を実行することに💪
今になってみれば、だったら最初からグリッドサーチ簡単にでもいいから使えよ、と我ながら思いました。

分かったことはこんな感じ。

  • Fold数=5、エポック数=20前後がいいらしい
  • スコアが頭打ちになった場合は、早期学習停止させると良い
  • 画像のオーグメンテーションは5種類前後の処理を軽~くだけ加えるのが良さそうだった
    • それ以上追加したり加工を強くすると精度が下がった
  • は1e-4でやっていた
    • が、これも3e-4や5e-4にしたほうが精度が上がった
    • 初期値は3e-4にして、学習率スケジューラーで自動調節を行うことに

という感じのことをやってみると、ギリ銅賞が取れそうか、取れなさそうか、といったラインに到達しました。

最終サブミットと結果

そして、この最終モデル調整が終わったのがコンペ終了の7時間前とかです。Kaggle上でノートブックを実行すること1~2時間、予測ファイルができたのでそれを提出。0.94→0.98とスコアが上昇しました。

その後、提出回数のリミットが余っていたので一応パラメータをちょっと変えてFacebookでも同じことをしようと思いましたが、謎にKaggleが固まってしまい学習に失敗。いっつもそうやってギリギリに何かをしようとするからこうなるんですね、はい。

まあ一応自分にできる限りのことはしたし、もういいかと思って結果を見てみると…

なんと順位が30番も上昇していました。

  • Private: 0.98128, Public: 0.98431
  • オープン枠5位、全体10位

っしゃーーーー!!!お前ら見たかコラ。マレーシア留学生舐めんなよ。フォォォォー( ^ω^ )( ^ω^ )( ^ω^ )


まあ冗談はさておき、銅賞がもらえれば御の字だと思っていたら、まさかの金賞🥇。自分でも信じられませんでした。

多分運が良かったんだと思います。その日は時間がなくてトイレに行くのも我慢していたので、ウン💩が溜まっていたんだと思います。

といいつつ、私と裏腹にかなりの人が順位を大幅に下げていた印象でした。過学習かなんかですかね。
私はPrivateでのデータも50%あるという情報を見ていたので、早期学習停止やらオーグメンテーションの導入やらで汎用性の高いモデルを作ってそれで逆転しようと考えていたのですが、その作戦が上手くいきました。

勝つために意識していたこと

大会に参加する上で意識していたことを書きます。

まず常識的に考えて、初心者なので自分で一からコーディングして戦うのは無理だなって思いました。なので、深層学習の詳しい仕組みは分からない代わりに、

  1. 大会の趣旨をメタ認知すること
  2. チュートリアルを元に、ルールの範囲内でChatGPTを活用すること

で経験不足を補って戦うことにしました。

特にこの1のメタ認知は強力で、これを上手く使えるようになるとどの課外活動でも実力以上の受賞ができます。
単純な実力で負けてるなら、別にそれ以外の部分で頭を使って勝てばいいんです。私が今までに色んなジャンルの課外活動で全国レベルの結果を出せたのも、このメタ認知をフル活用しているというのが大きいです。

メタ認知の活用と逆算

まず、運営が用意したデータや、学習済みモデルが制限されている意図を考えました。コーディングというよりは、戦略の面で効果を発揮しました。

まず、データの種類が少ないので、全部使わないと上位入賞は難しそうだというのはデータセットから逆算して分かるかと思います。

次に、こちらのコンペの目的を見てみましょう。

https://ioai-japan.org/

とりあえずAIの学習とかを普及したり推進したりしたいみたい。受験案内を見てみるとこんなものが。

https://ioai-japan.org/joai2025/

これを見る限り、(初回だから人が集めたいのだろうとも思いますが)初心者でも全然ウェルカムだよ!という姿勢が見て取れます。しかも、基本的には高校生以下がメインの大会です。

ここから、

  1. 機械学習初心者や高校生はスペックの高いパソコンを持っていないと思われる
  2. 使用可能なモデルも制限されている

ということを考えると、あまりGPUリソースなどに依存しないコンペなのだろうということが予想されます。逆にそうじゃなかったらガチ勢以外残らないので、普及の目的に反しちゃうでしょうから。

つまりこれはKaggle上のGPUだけで十分勝負できる大会ってことです。なので、これを踏まえて私は安心して学習時間でのゴリ押しではなくアイデア重視に切り替えようという発想ができたわけです。

そして、既にチュートリアルのF値が既に9割近かったということもポイントです。ここから、かなり上位は均衡したスコアになることは簡単に予想できます。しかも、特徴量エンジニアリングの工夫もあまりする余地がない。
要するに、みんな似たようなスコアやモデルになると思われるので、PublicとPrivateが50%ずつの評価ということを踏まえると、これは運営からの「汎用性が高いモデルかどうかで差をつけますよ~」というメッセージなのだということが読み取れます。

だから、ここまでの考察を踏まえてハイパーパラメーターのチューニングを泥臭く行ったり、仮にスコアが上がらなくても画像のオーグメンテーションを頑張ったのです。結果としてこれが作戦勝ちにつながりました。

こういう感じで大会の意図などを読み取りつつ求められているものを逆算するとどんな課外活動でも割と成果が出るっていう裏ワザです。良い子はぜひマネしてみてね。

生成AIの活用

また、先述の通り、基本的にコードの改良はチュートリアルを元にして、ChatGPTを活用しながら手探りで行いました。

このチュートリアルで9割近く出ていたため、おおよそこのコードの方針に従っていけば間違いないだろうと思ったからです。

  1. まずは自分で中身を読んで理解してみる
  2. 分からないところが出てくるたびに、ChatGPTに質問 or ググる
  3. 結果、なんとなくやっていること自体は分かるようになったので、中身を自分でいじってみるフェーズに突入
  4. 深層学習のことは分からないが、GCIで習ったデータサイエンスの基礎知識は使えそうだったので使ってみた
    1. 何か特徴量を自分で作ったらいいんじゃね?
    2. チュートリアルのモデルをそのまま重ねてアンサンブルさせたら精度上がるんじゃね?
    3. ハイパラを変えてみたら精度上がるんじゃね? …などなど
  5. あとは手動で実験を繰り返すのみ

これを地道にやっていったら上位勢とも張り合えるようになったんですが、最近の生成AIは恐ろしいですね。難しいことをメチャクチャ分かりやすく教えてくれるんですもん。

これから考えるに、もう今の時代のエンジニアに必要なものは自分でいかにコードを書けるかどうかというよりはAIの出力を理解してそれを効率よく手直ししていくスキルなのではないでしょうか。

スポンサーリンク

JOAI2025に参加しての感想

JOAI2025、メッチャ楽しかったです!深層学習に触れるのは初めてでしたが、このコンペがきっかけで一気に学習の興味・モチベも湧きました。

初心者だからチュートリアルコードを読むだけでも一苦労だったんですが、ChatGPTとかに分からないところを質問しながら進めていくうちに、なんとか上位勢とも張り合えるレベルまで持っていけたんです。結果、金賞を取れて本当に良い成功体験になったし、自信も付きました!

でも、反省点もあって。やっぱりいつも通り計画性がなかったのはダメでしたね。大学の課題が普通に忙しくて、スケジュール調整が全然できなかった。
Kaggle初心者だから、実行環境のエラーで中断とかもあって、焦る場面も多かったです。あともう数日早く始められていれば、さらに良いスコアが出せたんじゃないかって思うんですよね。やっぱり余裕を持った計画性って大事だなと痛感しました。

それから、最後はやっぱり運もあったかなと。深層学習を全く触ったことがない私が、まさか受賞、しかも金賞を取れるなんて自分でも思ってもいませんでした。
でも、確かにここまで来られたのは運の要素も大きいかもしれないけど、この経験をバネにこれからも精進しようと思います!

最後に、運営の方々、このような機会を設けてくださってありがとうございました。また来年もやりたいです!

スポンサーリンク

この記事が気に入ったら
フォローしてね!

よかったらSNSでシェアしてね!
  • URLをコピーしました!
目次