import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# ハイパーパラメータ
batch_size = 64
learning_rate = 0.001
num_epochs = 20

# STL-10のデータセットとデータローダーの準備
transform = transforms.Compose([
    transforms.Resize((96, 96)),  # STL-10の画像サイズに合わせる
    transforms.ToTensor(),
    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)),  # 正規化
])

train_dataset = datasets.STL10(root='./data', split='train', download=False, transform=transform)
test_dataset = datasets.STL10(root='./data', split='test', download=False, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

########################## ここにモデルの class を書いてください． ############################

class ResNet(torch.nn.Module):
    def __init__(self):
        super(ResNet, self).__init__()
        self.conv1 = torch.nn.Conv2d(3, 16, 3, 1, 1, bias=False)
        self.bn1 = torch.nn.BatchNorm2d(16)
        self.pool1 = torch.nn.MaxPool2d(2,2)
        self.bigblock2 = self.make_bigblock(16, 32, 7)
        self.pool2 = torch.nn.MaxPool2d(2,2)
        self.bigblock3 = self.make_bigblock(32, 64, 7)
        self.fc = torch.nn.Linear(64, 10)
    
    def make_bigblock(self, in_channels, out_channels, num_blocks):
        blocks = []
        for i in range(num_blocks - 1):
            blocks.append(block(in_channels, in_channels))
        blocks.append(block(in_channels, out_channels))
        return torch.nn.Sequential(*blocks)

    def forward(self, x):
        x = torch.relu(self.bn1(self.conv1(x)))
        x = self.pool1(x)
        x = self.bigblock2(x)
        x = self.pool2(x)
        x = self.bigblock3(x)
        x = torch.mean(x, dim=(2,3))
        x = self.fc(x)
        return x

class block(torch.nn.Module):
    def __init__(self, in_channels, out_channels):
        super(block, self).__init__()
        self.conv1 = torch.nn.Conv2d(in_channels, in_channels, 3, 1, 1, bias=False)
        self.conv2 = torch.nn.Conv2d(in_channels, out_channels, 3, 1, 1, bias=False)
        self.bn1 = torch.nn.BatchNorm2d(in_channels)
        self.bn2 = torch.nn.BatchNorm2d(out_channels)
        if in_channels == out_channels:
            self.shortcut = torch.nn.Sequential()
        else:
            self.shortcut = torch.nn.Conv2d(in_channels, out_channels, 1, 1, 0, bias=False)

    def forward(self, x):
        t = torch.relu(self.bn1(self.conv1(x)))
        x = torch.relu(self.bn2(self.shortcut(x) + self.conv2(t)))
        return x

#########################################################################################

# モデル、損失関数、オプティマイザの設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = ResNet().to(device)
print(model)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=2e-5)

# 学習ループ
def train_model():
    model.train()
    for epoch in range(num_epochs):
        total_loss = 0
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)
            
            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(train_loader):.4f}")

# テストループ
def test_model():
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print(f"Test Accuracy: {100 * correct / total:.2f}%")

# 実行
if __name__ == "__main__":
    train_model()
    test_model()
