개발이모저모

[머신러닝] 맥미니 M1 vs 맥북프로 M1 Pro vs 구글 코랩 성능 비교!

아티스트갓건 2022. 12. 8. 13:55

머신러닝을 하시는 분들 중 M1칩이 탑재 된 맥 컴퓨터를 사려고 하시는 분들께 조금이라도 도움이 되길 바라며 이 글을 쓴다

Pytorch 또는 Tensorflow의 기본 딥러닝 예제들을 통해 맥미니, 맥북프로, 구글 코랩의 성능을 시간으로 측정하였다

맥미니는 내꺼고 맥북 프로는 아는형님꺼, 구글 코랩은 구글꺼다

 

미리 보는 결론

적당히 머신러닝&딥러닝을 공부하거나 테스트 할땐 맥미니 or 아무컴 + 코랩을 사용하는게 좋고

적당히 할 생각이 없다면 맥북프로 이상 or 머신러닝 전용 컴퓨터를 사용하는게 좋다!

 

테스트 컴퓨터

1. 맥미니

https://www.apple.com/kr/shop/buy-mac/mac-mini/apple-m1-%EC%B9%A9(8%EC%BD%94%EC%96%B4-cpu-%EB%B0%8F-8%EC%BD%94%EC%96%B4-gpu)-256gb 

 

Mac mini

새로운 Apple M1 칩을 탑재한 Mac mini는 그 어느 때보다 빨라진 성능과 완전히 새로워진 능력을 선사합니다. 지금 온라인으로 구입하세요.

www.apple.com

2. 맥북 프로

https://www.apple.com/kr/shop/buy-mac/macbook-pro/14%ED%98%95-%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4-%EA%B7%B8%EB%A0%88%EC%9D%B4-10%EC%BD%94%EC%96%B4-cpu-16%EC%BD%94%EC%96%B4-gpu-1tb

 

14형 MacBook Pro - 스페이스 그레이

강력한 M1 Pro 칩 또는 M1 Max 칩을 탑재한 MacBook Pro. 놀라운 배터리 사용 시간과 Liquid Retina XDR 디스플레이를 자랑합니다. 지금 구입하세요.

www.apple.com

3. 구글 코랩

https://colab.research.google.com/

 

Google Colaboratory

 

colab.research.google.com

4. 비교

  CPU GPU NeuralEngine Price
맥미니 m1 8 Core 8 Core 16 Core 890,000 
맥북프로 m1pro 10 Core 16 Core 16 Core 3,360,000
코랩 Intel Xeon 2.3Ghz 2Core Nvidia Tesla T4 8GB - 0

근본없이 생각하자면 맥미니와 맥북프로의 GPU 코어가 2배 차이나므로, 맥북프로가 두배 이상 빨라야 된다. 

 

모델 테스트

이제 Torch와 Tensorflow의 각각의 예제 코드를 이용해 시간을 측정해보자!

아는 형님의 맥북 프로를 빌린거기 때문에 예제를 3개밖에 돌리진 못했다. 이후 아래에 작성된 코드들은 예제코드를 그대로 사용했거나 순수 머신러닝 속도를 위해서 약간 수정되었고, 코드 그대로 복사해서 코랩이나 여러분의 환경에서 사용할 수 있다.

 

1. fashion mnist by torch

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
import torch.backends.mps
import time

training_data = datasets.FashionMNIST(
    root = 'data',
    train = True,
    download = True,
    transform= ToTensor(),
)

test_data = datasets.FashionMNIST(
    root = 'data',
    train = False,
    download = True,
    transform= ToTensor()
)

start = time.time()

BATCH_SIZE = 64

training_dataloader = DataLoader(training_data, batch_size=BATCH_SIZE)
test_dataloader = DataLoader(test_data, batch_size=BATCH_SIZE)

for X, y in test_dataloader:
    print(f'Shape of X [N, C, H, W] : {X.shape}')
    print(f'Shape of y : {y.shape} {y.dtype}')
    break

device = "mps" if torch.backends.mps.is_available() and torch.backends.mps.is_built() else 'cpu'
print(device)

class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
        )
    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits
    
model = NeuralNetwork().to(device)

# print(model)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 1e-3) # le-3 : 0.001

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    model.train()
    for batch, (X,y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        
        pred = model(X)
        loss = loss_fn(pred, y)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f'loss : {loss:>7f} [{current:>5d}/{size:>5d}]')

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0,0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f'Test Error : \n Accuracy: {(100 * correct) :>0.1f}%, Avg Loss : {test_loss:>8f}\n')
    
EPOCHS = 10

for t in range(EPOCHS):
    print(f'Epoch {t+1}\n----------------------------')
    train(training_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
print('done!')
print(time.time() - start)
  Fashion Mnist by Torch
맥미니 (1등)
42.03784894943237
맥북프로 (2등)
76.67609977722168
코랩 (3등)
91.44875264167786

???

제일 느려야 될 맥미니가 제일 빨랐다..

코드는 모두 동일하고, 데이터셋 다운로드의 시간은 아무래도 코랩이 불리할테니 데이터셋 다운로드 이후에 시간 측정을 시작한 결과이다.

몇번이나 돌려봤는데도 비슷했다..

 

2. mnist by torch

from __future__ import print_function
import argparse
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.optim.lr_scheduler import StepLR


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output


def train(args, 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)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
            if args.dry_run:
                break


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.nll_loss(output, target, reduction='sum').item()  # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True)  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

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


def main():
    # Training settings
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
    parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                        help='input batch size for training (default: 64)')
    parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                        help='input batch size for testing (default: 1000)')
    parser.add_argument('--epochs', type=int, default=5, metavar='N',
                        help='number of epochs to train (default: 14)')
    parser.add_argument('--lr', type=float, default=1.0, metavar='LR',
                        help='learning rate (default: 1.0)')
    parser.add_argument('--gamma', type=float, default=0.7, metavar='M',
                        help='Learning rate step gamma (default: 0.7)')
    parser.add_argument('--no-cuda', action='store_true', default=False,
                        help='disables CUDA training')
    parser.add_argument('--no-mps', action='store_true', default=False,
                        help='disables macOS GPU training')
    parser.add_argument('--dry-run', action='store_true', default=False,
                        help='quickly check a single pass')
    parser.add_argument('--seed', type=int, default=1, metavar='S',
                        help='random seed (default: 1)')
    parser.add_argument('--log-interval', type=int, default=10, metavar='N',
                        help='how many batches to wait before logging training status')
    parser.add_argument('--save-model', action='store_true', default=False,
                        help='For Saving the current Model')
    # args = parser.parse_args()
    args, unknown = parser.parse_known_args()

    use_cuda = not args.no_cuda and torch.cuda.is_available()
    use_mps = not args.no_mps and torch.backends.mps.is_available()

    torch.manual_seed(args.seed)

    if use_cuda:
        device = torch.device("cuda")
    elif use_mps:
        device = torch.device("mps")
    else:
        device = torch.device("cpu")

    train_kwargs = {'batch_size': args.batch_size}
    test_kwargs = {'batch_size': args.test_batch_size}
    if use_cuda:
        cuda_kwargs = {'num_workers': 1,
                       'pin_memory': True,
                       'shuffle': True}
        train_kwargs.update(cuda_kwargs)
        test_kwargs.update(cuda_kwargs)

    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
        ])
    dataset1 = datasets.MNIST('../data', train=True, download=True,
                       transform=transform)
    dataset2 = datasets.MNIST('../data', train=False,
                       transform=transform)
    train_loader = torch.utils.data.DataLoader(dataset1,**train_kwargs)
    test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)
    model = Net().to(device)
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)

    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)
    for epoch in range(1, args.epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
        scheduler.step()

    if args.save_model:
        torch.save(model.state_dict(), "mnist_cnn.pt")

import time
if __name__ == '__main__':
    print('GPU start!')
    start = time.time()
    main()
    gpu_time = time.time() - start
    print(f'GPU TIME : {gpu_time}')
  Mnist by Torch
맥미니 (1등)
58.34226679801941
맥북프로 (3등)
97.23483228683472
코랩 (2등)
83.0124397277832

??????

이번에도 맥미니의 압승이다. 심지어 성능이 가장 좋아야 될 맥북프로는 코랩보다도 느린 결과를 보였다.

하지만 위의 두 예제는 딥러닝용 예제 중 비교적 가벼운 모델에 속한다. 이번엔 좀 무거운 엔진을 돌려보았다

 

3. keras tuner by tensorflow

# !pip install keras-tuner # Colab에서 실행할 경우 이 코드를 실행하여 keras tuner를 설치하자

import tensorflow as tf
import kerastuner as kt
import time
import IPython.display
(img_train, label_train), (img_test, label_test) = tf.keras.datasets.fashion_mnist.load_data()

img_train = img_train.astype('float32') / 255.0
img_test = img_test.astype('float32') / 255.0

start = time.time()
def model_builder(hp):
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten(input_shape = (28, 28)))
    
    hp_units = hp.Int('units', min_value = 32, max_value = 512, step = 32)
    model.add(tf.keras.layers.Dense(units = hp_units, activation = 'relu'))
    model.add(tf.keras.layers.Dense(10))
    
    hp_learning_rate = hp.Choice('learning_rate', values = [1e-2, 1e-3, 1e-4])
    
    model.compile(optimizer = tf.keras.optimizers.Adam(learning_rate=hp_learning_rate),
                  loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits = True),
                  metrics = ['accuracy'])
    
    return model

tuner = kt.Hyperband(model_builder,
                     objective='val_accuracy',
                     max_epochs= 10,
                     factor = 3,
                     directory = 'my_dir',
                     project_name = 'intro_to_kt')
class ClearTrainingOutput(tf.keras.callbacks.Callback):
    def on_train_end(*arg, **kwargs):
        IPython.display.clear_output(wait = True)
        
tuner.search(
    img_train, label_train, 
    epochs = 10, 
    validation_data = (img_test, label_test),
    callbacks = [ClearTrainingOutput()])

best_hps = tuner.get_best_hyperparameters(num_trials = 1)[0]
print(f"""
The hyperparameter search is complete. The optimal number of units in the first densely-connected
layer is {best_hps.get('units')} and the optimal learning rate for the optimizer
is {best_hps.get('learning_rate')}.
""")
search_time = time.time() - start
model = tuner.hypermodel.build(best_hps)
model.fit(img_train, label_train,
          epochs = 10,
          validation_data = (img_test, label_test))

print(f'search_time : {search_time}')
print('all time : ', time.time() - start)

 

  Keras-Tuner by Tensorflow
맥미니 (3등)
1146.9256701469421 / 
1243.9554872512817
맥북프로 (1등) 242.5776960849762 /
268.58068108558655
코랩 (2등) 846.2304947376251 /
894.6409358978271

오호

드디어 원하던 결과가 나왔다. 위의 예제는 Keras-Tuner라고 해서 코드를 돌릴때 자동으로 layers 갯수 등의 모델 파라메터를 최적화해주는 코드이다. 약간 비지도학습 K-Means Clustering을 할 때, 자동으로 K 값을 찾아주는 알고리즘과 유사하다.

엔진 특성상 적정 모델 파라메터를 찾기 위해 모델을 여러번 돌리기 때문에, 위의 Mnist나 Fashion Mnist와는 비교도 안될만큼 연산량이 많이 필요하다.

시간이 두개인 이유는 적정 파라메터를 찾는 시간 / 적정 파라메터를 적용한 모델의 시간이라서 그렇다.

맥미니의 경우 파라메터 찾는데 1146초(약 12분)이 걸렸고, 파라메터를 적용한 모델을 한번 돌릴때 100초 (약 1분 40초) 정도 걸린다.

코랩은 파라메터 찾는데 846초, 모델을 돌릴때 50초 정도 걸린다.

맥북프로는 파라메터 찾는데 242초, 모델을 돌릴때 26초 정도 걸린다.

 

무거운 모델일수록 맥북프로 > 코랩 > 맥미니 이렇게 성능이 좋지 않을까 생각된다.

 

 

결론

더 많은 모델을 돌려보지 못했고, 다양한 분야의 모델을 돌리지 못한 것도 아쉽지만

위의 3개의 예제를 돌려본 결과로 유추할 수 있는건, 고만고만한 모델을 돌릴땐 성능이 가장 좋은 맥북프로가 오히려 큰 효과를 내지 않는다는 것이고, 모델이 커지면 커질수록 맥북프로의 진가가 발휘한다는 것이다. nvidia 그래픽카드가 탑재된 컴퓨터도 돌려보고 싶은데 아쉽게도 구할 방법도 부탁할 사람도 없다....

 

만약 본인이 혼자 머신러닝을 공부하고 테스트 하는 사람이라면 맥미니를 사거나 아무 컴퓨터 + 구글 코랩으로 테스트 하는게 경제적으로 이득이다.

본인이 혼자 머신러닝을 하는데 좀 개인 사업적으로 접근하는 사람이라면 맥북프로 이상을 사거나 아주 좋은 그래픽카드를 갖춘 상태에서 작업하는게 좋을것 같다.