見出し画像

遅ればせながらclineを試す。(おまけテトリス)

巷で話題のコーディング エージェント clineを遅ればせながら、じぶんも試してみました。

インストール手順は多くのブログ記事がありますので、それらを参考にさせていただきました。今回は割愛します。
clineにはいくつかのフォークがあるようなのですが、今回はrecline <v0.2.5>を使ってみました。

はじめてのcline体験(MNIST)

まずは小手始めに、深層学習の教科書的なMNIST(手書き数字認識モデルの作成)の課題を丸投げでやらせてみることにします。

カスタム インストラクション

プラグインの設定画面に入力したプロンプトは以下の通りです。

日本語でやり取りしたい。使用言語は Python を優先。
仮想環境は uv を使用。大きな変更ごとに git にコミットすること。

Task(MNISTのモデル学習)

手書き数字を認識するモデルをmnistデータセットで作って。
精度は99%以上を目指して。フレームワークはpytorchでつくって。
使いそうなライブラリはインストール済みです。確認してください。

適宜gitコミットしながら、ガンガンコーディングして、エラーが出たら自動で修正してくれる。以前からよく似たことはChatGPTとかで実現できていたとはいえ、自分の環境で動いているのを見るとちょっと感動です。
サクッとベースラインのコードが走ってしまいました。

では、高みを目指してもらいましょう。

トレーニングは動作してaccは改善しているみたいですね。
では、徐々に精度向上にトライしてください。

いい感じのCNNのモデルの学習コードが生成されました。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, ConcatDataset
import matplotlib.pyplot as plt
import numpy as np

def cutmix_data(x, y, alpha=1.0):
    lam = np.random.beta(alpha, alpha)
    batch_size = x.size()[0]
    index = torch.randperm(batch_size)
    
    W, H = x.size()[2:]
    cut_rat = np.sqrt(1. - lam)
    cut_w = int(W * cut_rat)
    cut_h = int(H * cut_rat)
    cx = np.random.randint(W)
    cy = np.random.randint(H)
    
    bbx1 = np.clip(cx - cut_w // 2, 0, W)
    bby1 = np.clip(cy - cut_h // 2, 0, H)
    bbx2 = np.clip(cx + cut_w // 2, 0, W)
    bby2 = np.clip(cy + cut_h // 2, 0, H)
    
    x_cutmix = x.clone()
    x_cutmix[:, :, bbx1:bbx2, bby1:bby2] = x[index, :, bbx1:bbx2, bby1:bby2]
    
    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))
    y_a, y_b = y, y[index]
    
    return x_cutmix, y_a, y_b, lam

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # より大きなモデル容量
        self.conv1 = nn.Conv2d(1, 96, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(96)
        self.conv2 = nn.Conv2d(96, 192, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(192)
        self.conv3 = nn.Conv2d(192, 192, 3, padding=1)
        self.bn3 = nn.BatchNorm2d(192)
        self.conv4 = nn.Conv2d(192, 384, 3, padding=1)
        self.bn4 = nn.BatchNorm2d(384)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(384 * 3 * 3, 512)
        self.fc2 = nn.Linear(512, 10)
        self.dropout1 = nn.Dropout(0.2)
        self.dropout2 = nn.Dropout(0.4)

    def forward(self, x):
        batch_size = x.size(0)
        
        x = self.pool(F.relu(self.bn1(self.conv1(x))))
        x = self.dropout1(x)
        x = self.pool(F.relu(self.bn2(self.conv2(x))))
        x = self.dropout1(x)
        x = F.relu(self.bn3(self.conv3(x)))
        x = self.dropout1(x)
        x = self.pool(F.relu(self.bn4(self.conv4(x))))
        x = x.view(batch_size, 384 * 3 * 3)
        x = F.relu(self.fc1(x))
        x = self.dropout2(x)
        x = self.fc2(x)
        return x

def train(model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        
        if np.random.random() > 0.5:
            data, target_a, target_b, lam = cutmix_data(data, target)
            optimizer.zero_grad()
            output = model(data)
            loss = lam * F.cross_entropy(output, target_a) + (1 - lam) * F.cross_entropy(output, target_b)
        else:
            optimizer.zero_grad()
            output = model(data)
            loss = F.cross_entropy(output, target)
        
        loss.backward()
        optimizer.step()
        
        if batch_idx % 100 == 0:
            print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} '
                  f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')

def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.cross_entropy(output, target, reduction='sum').item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    accuracy = 100. * correct / len(test_loader.dataset)
    print(f'\nTest set: Average loss: {test_loss:.4f}, '
          f'Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
    return accuracy, test_loss

def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Using device: {device}")

    # データ拡張の定義
    train_transform = transforms.Compose([
        transforms.RandomAffine(
            degrees=15,
            translate=(0.15, 0.15),
            scale=(0.85, 1.15),
            shear=(-10, 10)
        ),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,)),
        transforms.RandomErasing(p=0.3, scale=(0.02, 0.1), ratio=(0.3, 3.3))
    ])

    test_transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])

    # データセットの読み込み
    mnist_train = datasets.MNIST('data', train=True, download=True, transform=train_transform)
    emnist_digits_train = datasets.EMNIST('data', split='digits', train=True, download=True, transform=train_transform)
    emnist_mnist_train = datasets.EMNIST('data', split='mnist', train=True, download=True, transform=train_transform)
    
    # テストデータ
    mnist_test = datasets.MNIST('data', train=False, transform=test_transform)
    emnist_digits_test = datasets.EMNIST('data', split='digits', train=False, transform=test_transform)
    emnist_mnist_test = datasets.EMNIST('data', split='mnist', train=False, transform=test_transform)

    # データセットの結合
    train_dataset = ConcatDataset([mnist_train, emnist_digits_train, emnist_mnist_train])
    test_dataset = ConcatDataset([mnist_test, emnist_digits_test, emnist_mnist_test])

    print(f"Combined training dataset size: {len(train_dataset)}")
    print(f"Combined test dataset size: {len(test_dataset)}")

    train_loader = DataLoader(
        train_dataset,
        batch_size=64,
        shuffle=True,
        pin_memory=True,
        drop_last=True,
        num_workers=2
    )
    test_loader = DataLoader(
        test_dataset,
        batch_size=1000,
        num_workers=4,
        pin_memory=True
    )

    model = Net().to(device)
    optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)
    
    # より長いスケジュール
    scheduler = optim.lr_scheduler.OneCycleLR(
        optimizer,
        max_lr=0.001,
        epochs=100,  # エポック数を増やす
        steps_per_epoch=len(train_loader),
        pct_start=0.3,
        anneal_strategy='cos'
    )

    accuracies = []
    losses = {'train': [], 'test': []}
    best_accuracy = 0
    best_loss = float('inf')
    epochs_without_improvement = 0
    max_epochs_without_improvement = 15  # より長い忍耐
    patience_threshold = 0.0001

    for epoch in range(1, 101):
        model.train()
        train_loss = 0
        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = data.to(device), target.to(device)
            
            if np.random.random() > 0.5:
                data, target_a, target_b, lam = cutmix_data(data, target)
                optimizer.zero_grad()
                output = model(data)
                loss = lam * F.cross_entropy(output, target_a) + (1 - lam) * F.cross_entropy(output, target_b)
            else:
                optimizer.zero_grad()
                output = model(data)
                loss = F.cross_entropy(output, target)
            
            train_loss += loss.item()
            loss.backward()
            optimizer.step()
            scheduler.step()
            
            if batch_idx % 100 == 0:
                print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} '
                      f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')
        
        train_loss /= len(train_loader)
        losses['train'].append(train_loss)
        
        accuracy, test_loss = test(model, device, test_loader)
        accuracies.append(accuracy)
        losses['test'].append(test_loss)
        
        if test_loss < best_loss - patience_threshold:
            best_loss = test_loss
            if accuracy > best_accuracy:
                best_accuracy = accuracy
                torch.save({
                    'epoch': epoch,
                    'model_state_dict': model.state_dict(),
                    'optimizer_state_dict': optimizer.state_dict(),
                    'accuracy': accuracy,
                    'loss': test_loss,
                }, 'mnist_model_best.pth')
            epochs_without_improvement = 0
        else:
            epochs_without_improvement += 1
            
        if epochs_without_improvement >= max_epochs_without_improvement:
            print(f"Early stopping triggered. Best accuracy: {best_accuracy:.2f}%, Best loss: {best_loss:.4f}")
            break

        current_lr = optimizer.param_groups[0]['lr']
        print(f"Current learning rate: {current_lr:.6f}")

    # 学習曲線のプロット
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
    
    ax1.plot(accuracies, label='Test Accuracy')
    ax1.set_title('Model Accuracy Progress')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Accuracy (%)')
    ax1.grid(True)
    ax1.legend()
    
    ax2.plot(losses['train'], label='Train Loss')
    ax2.plot(losses['test'], label='Test Loss')
    ax2.set_title('Model Loss Progress')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Loss')
    ax2.grid(True)
    ax2.legend()
    
    plt.tight_layout()
    plt.savefig('training_progress.png')
    plt.close()

if __name__ == '__main__':
    main()
いい感じ。少なくとも私より間違いなく優秀😢。では、もう一声がんばってもらいましょう。
EMNIST Digits EMNIST MNIST のデータセットも使って
さらなる高みを目指して
いい感じにコード修正してgit commitもしてくれます。あと一声がんばってもらいましょう。
MNISTタスクのモデルをCNNから
トランスフォーマーを利用したViTに変えて性能を比較したいです。
import torch
import torch.nn as nn
from einops import repeat
from einops.layers.torch import Rearrange
from .layers import Transformer

class ViT(nn.Module):
    def __init__(
        self,
        *,
        image_size=28,
        patch_size=4,
        num_classes=10,
        dim=256,
        depth=6,
        heads=8,
        mlp_dim=512,
        pool='cls',
        channels=1,
        dim_head=32,
        dropout=0.1,
        emb_dropout=0.1
    ):
        super().__init__()
        image_height, image_width = image_size, image_size
        patch_height, patch_width = patch_size, patch_size
        
        assert image_height % patch_height == 0 and image_width % patch_width == 0, \
            'Image dimensions must be divisible by the patch size.'

        num_patches = (image_height // patch_height) * (image_width // patch_width)
        patch_dim = channels * patch_height * patch_width

        self.to_patch_embedding = nn.Sequential(
            Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', 
                     p1=patch_height, p2=patch_width),
            nn.LayerNorm(patch_dim),
            nn.Linear(patch_dim, dim),
            nn.LayerNorm(dim),
        )

        self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
        self.dropout = nn.Dropout(emb_dropout)

        self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim, dropout)

        self.pool = pool
        self.to_latent = nn.Identity()

        self.mlp_head = nn.Sequential(
            nn.LayerNorm(dim),
            nn.Linear(dim, num_classes)
        )

    def forward(self, img):
        x = self.to_patch_embedding(img)
        b, n, _ = x.shape

        cls_tokens = repeat(self.cls_token, '1 1 d -> b 1 d', b=b)
        x = torch.cat((cls_tokens, x), dim=1)
        x += self.pos_embedding[:, :(n + 1)]
        x = self.dropout(x)

        x = self.transformer(x)

        x = x.mean(dim=1) if self.pool == 'mean' else x[:, 0]

        x = self.to_latent(x)
        return self.mlp_head(x)

へぇ、ViTってこんな感じの実装なんですね。

Vision TransformerをMNISTタスクに実装し、CNNモデルと比較しました:アーキテクチャの実装
4x4のパッチサイズ
6層のTransformerエンコーダー
8ヘッドのself-attention
CLSトークンによる分類
性能比較
ViT: 99.76%(Best accuracy)
CNN: 99.52%(Previous best)
0.24%の精度向上を達成
コードの整理
models/: モデル関連の実装
utils/: データ処理と学習関連の実装
train.py: メインの学習スクリプト

ViTはCNNと比較して優れた性能を示し、画像認識タスクにおけるTransformerの有効性が確認されました。

ほぼ自動でここまでできちゃいました。SoTAにちょっと届きませんでしたが、学部の学生実験レポートくらいのレベルはありそう。うーん。エージェントのすごさを十分堪能できました。

今回はClaude 3.5 sonnetを使いましたが、コスト度返しにして、OpenAI o1やら、近く公表されるo3を使ったら、凄いことになりそうですね。(語彙不足で恐縮です😅)

リポジトリはこちら

おまけ。テトリスも作ってもらいました。

現場からは以上です。


いいなと思ったら応援しよう!

この記事が参加している募集