遅ればせながら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 のデータセットも使って
さらなる高みを目指して
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ってこんな感じの実装なんですね。
ほぼ自動でここまでできちゃいました。SoTAにちょっと届きませんでしたが、学部の学生実験レポートくらいのレベルはありそう。うーん。エージェントのすごさを十分堪能できました。
今回はClaude 3.5 sonnetを使いましたが、コスト度返しにして、OpenAI o1やら、近く公表されるo3を使ったら、凄いことになりそうですね。(語彙不足で恐縮です😅)
リポジトリはこちら
おまけ。テトリスも作ってもらいました。
現場からは以上です。