🚀

国際人工知能オリンピックのサンプル問題を解いてみた

2024/04/07に公開

国際人工知能オリンピック(IOAI)が今年第1回の開催を迎えるとのことで、公式HPに紹介されているサンプル問題を解いてみます。

IOAIについて

今年から始まる、科学オリンピックのうちの1つです。今年はブルガリアのブルガスで開催されるようです。

https://ioai-official.org/

肝心のコンテストの内容ですが、科学ラウンドと実践ラウンドの2つがあります。科学ラウンドは機械学習、深層学習に関するipynbが提供され、それを解きます。実践ラウンドはChatGPTを始めとするGUIアプリケーションを活用した科学的な問題について考察します。

いわゆるKaggle的なコーディング要素が求められるのは前者の科学ラウンドのようです。

競技AIというとKaggleの印象が強いですが、IOAIはどんな事を問われるのか気になるところです。

公式HPには3問掲載されていました。

  1. NLPタスク(言語モデルの訓練、論文の再実装)
  2. NLPタスク(単語埋込のバイアス除去)
  3. 画像タスク(Adversarial Attack) <- これをやる

今回扱う問題は次のcolabリンクに掲載されています。(正解はなさそう)

https://colab.research.google.com/drive/1yFzMkHsmnLPVPilrJo9oF5usEfGeXRii?usp=sharing

Task1 CNNモデルの作成

要件

  • CIFAR-10データセットを読み込む
  • ResNet-18モデルを使ってCIFAR-10訓練セットを訓練する
  • CIFAR-10評価セットでモデルの性能を評価する
  • 評価セットでAcc 80%を超えること
  • モデルのアーキテクチャを変更しないこと

このコードだけ与えられます。

from torchvision.models import resnet18
net = resnet18(num_classes=10).cuda()

予備実験でAugmentationとschedulerなしで訓練してみましたが、性能が70%未満だったのでちゃんと工夫しないといけなさそうです。

3時間くらい奮闘したんですが、全然80%を超えてくれないので意外と難しいみたいです。何で?

以下の条件で訓練しました。

  • Data Augmentation
    • クロップ、フリップ、回転、アフィン変換、色Jitter、正規化
  • Adam (lr: 1e-1) + OneCycleLRスケジューラ
  • バッチサイズ: 512, epoch: 100
  • NLLLoss

80%超えた(๑•̀ㅂ•́)و✧

ログ
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:19<00:00,  5.16it/s]
Epoch 100, Loss: 0.36403972488276815

Accuracy on the test set: 82.61%
コード
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
from torch.functional import F
from tqdm import tqdm

stats = ((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))

# Define transformations for the CIFAR-10 dataset
train_transform = transforms.Compose([
    transforms.RandomCrop(32, padding=4),

    transforms.RandomHorizontalFlip(), # FLips the image w.r.t horizontal axis
    transforms.RandomRotation((-7,7)),     #Rotates the image to a specified angel
    transforms.RandomAffine(0, shear=10, scale=(0.8,1.2)), #Performs actions like zooms, change shear angles.
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), # Set the color params
    transforms.ToTensor(),
    transforms.Normalize(*stats)
])
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(*stats)
])

# Load CIFAR-10 dataset
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=512, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=512, shuffle=False, num_workers=4)
net = resnet18(num_classes=10).cuda()
# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=1e-1)
epochs = 100
sched = torch.optim.lr_scheduler.OneCycleLR(optimizer, 1e-1, epochs=epochs,
                                                steps_per_epoch=len(train_loader))

def train_model(model, sched, train_loader, criterion, optimizer, num_epochs=10):
    model.train()
    for epoch in range(num_epochs):
        running_loss = 0.0
        for images, labels in tqdm(train_loader):
            images, labels = images.cuda(), labels.cuda()
            outputs = model(images)
            loss = F.nll_loss(F.log_softmax(outputs), labels)

            loss.backward()

            optimizer.step()

            optimizer.zero_grad()

            running_loss += loss.item()
        print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader)}')
        sched.step()

        evaluate_model(model, test_loader)

def evaluate_model(model, test_loader):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.cuda(), labels.cuda()
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print(f'Accuracy on the test set: {100 * correct / total}%')

# Train and evaluate the model
evaluate_model(net, test_loader)
train_model(net, sched, train_loader, criterion, optimizer, num_epochs=epochs)

Task2 Adversarial Exampleの作成

\epsilonを[0.25, 1.0, 1.5]に設定し、Adversarial Exampleを作成したときの評価セットの正解率を提出します。(オリジナル画像とAdversarial Exampleとのl2距離を\epsilonとしている。)
低ければ低いほど良いようです。

colabで貼られている文献を読む限り、The Fast Gradient Sign Method (FGSM)を試せば良いことがわかりました。FGSMは最もシンプルなAdversarial Attackの方法で、勾配を上昇する方向にサンプルにノイズを付与します。

https://adversarial-ml-tutorial.org/adversarial_examples/

紹介されていたFGSM(eps=0.1)をそのまま適用させてみたところ、ほとんどのサンプルが間違えるようになりました。

Before

After

結果は次のようになりました。乱択よりも悪い結果になっていてすごい。

そもそも正規化したデータに対して適用させてよかったんだっけ? epsilonの取り方正しいか不安になってきました。間違ってるかもしれないです...... 申し訳ない......

=== eps=0.25 ===
Accuracy on the test set: 7.06%
=== eps=1.0 ===
Accuracy on the test set: 6.33%
=== eps=1.5 ===
Accuracy on the test set: 7.02%
コード
def fgsm(model, X, y, epsilon):
    """ Construct FGSM adversarial examples on the examples X"""
    delta = torch.zeros_like(X, requires_grad=True)
    loss = nn.CrossEntropyLoss()(model(X + delta), y)
    loss.backward()
    return epsilon * delta.grad.detach().sign()

# Function to evaluate the model
def evaluate_adversarial_model(model, test_loader, eps=0.01):
    model.eval()
    correct = 0
    total = 0
    # with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.cuda(), labels.cuda()
        # print(images.shape)
        delta = fgsm(net, images, labels, eps)

        outputs = model(images + delta)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    print(f'=== {eps=} ===')
    print(f'Accuracy on the test set: {100 * correct / total}%')

evaluate_adversarial_model(net, test_loader, eps=0.25)
evaluate_adversarial_model(net, test_loader, eps=1.0)
evaluate_adversarial_model(net, test_loader, eps=1.5)

Task3 応用したAdversarial Exampleの作成

一様な大きさでノイズを付与する従来のやり方を応用し、画像中心を小さいノイズ、外側を大きなノイズを付与するタスクです。

要件

  • \epsilon = \frac{2}{225} (画像中心16x16ピクセル)
  • \epsilon = \frac{8}{255} (その他)
  • \epsilonl\infty距離で定義される。

行列で係数マスクを作って符号と掛け合わせます。

epsilon = 8/255
epsilon_mask = torch.ones((3, 32, 32)) * epsilon
epsilon_mask[:, 8:24, 8:24] /= 4
plt.imshow(epsilon_mask[0])

82%->61%になりました。

Accuracy on the test set: 61.38%
コード
# Function to evaluate the model
def evaluate_adversarial_custom_model(model, test_loader, eps_mask):
    model.eval()
    correct = 0
    total = 0
    # with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.cuda(), labels.cuda()
        # print(images.shape)
        delta = fgsm(net, images, labels, 1) * eps_mask

        outputs = model(images + delta)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    # print(f'=== {eps=} ===')
    print(f'Accuracy on the test set: {100 * correct / total}%')

evaluate_adversarial_custom_model(net, test_loader, eps_mask=epsilon_mask.cuda())

Task4 カスタムモデルに対する攻撃

WIP

おわりに

気になるところがあれば気軽に連絡してください。epsilonの取り方は全く自信がありません。

そして、IOAIは4/17まで応募を受け付けているらしいです。

https://ioai-japan.notion.site/1-IOAI2024-7ee01907740141dcbcad2e3bc70b7210

Discussion