[初心者向け] Amazon SageMaker で PyTorch を使って Cifar10 を特徴抽出して画像分類してみる
はじめに
おはようございます、もきゅりんです。
最近は個人的な取り組みの一環として、機械学習の学習に取り組んでいます。
前回 は、XGBoost
で赤ワインの品質を分類しました。
今回は、PyTorch
をSageMaker(以下SM)で使ってみたく、みんな大好き Cifar10
*1を使って分類します。
基本的にやってることは下記 PyTorch
のチュートリアル内容です。
ただ、チュートリアルをやるだけでは物足りなかったので、転移学習 をやってみようと思った次第です。
転移学習の参考も同じく PyTorch
のチュートリアルです。
TRANSFER LEARNING FOR COMPUTER VISION TUTORIAL
画像分類では、ディープラーニングのConvolutional Neural Network(以下CNN)という手法を利用することが多いようです。
なお、ここではCNNがどのようなものかを改めて説明しません。(ネットにいくらでも説明資料があるため)
PyTorchをSMを使って転移学習で画像分類するが趣旨になります。
なお、自分は専門的なデータサイエンティストでも何でもないので、無駄、非効率な作業を行っているかもしれない点、ご了承下さい。
転移学習とは
転移学習もいくらでも説明資料はネットにありそうですが、簡潔に説明しておきます。
転移学習とは、ある問題を解決するのに学習した知識を、別の関連した問題の解決に適用することです。
たとえば、車を認識することを学んだ知識は、トラックを認識しようとするときに適用できます。
さまざまな動物の画像が集まった巨大なデータセットで学習した知識は、犬や猫の分類にも役立ちそうですよね。
ここで扱われている知識とは、ネットワーク(モデル) *2と置き換えて貰えればと思います。
画像分類の転移学習でよく利用されるのは、 ImageNet
という140万以上の巨大な日常的な画像データセットで学習されたネットワークです。
PyTorch
では、そのような事前に学習したネットワーク(モデル)を簡単に利用したネットワーク(モデル)を構築することができます。 *3
転移学習の手法について
転移学習の手法は自分の知る限り、2つあります。
feature extraction (特徴抽出) と fine-tuning (ファインチューニング) です。 *4
feature extraction
は事前に学習されたネットワーク最終層の分類器のみを今回のデータセット用に取り替えて、改めて学習させる手法です。
最終層以外のネットワーク層は「そのまま」で利用します。(学習させてしまうと、これまで学習してきた知識がパーになってしまいます)
fine-tuning
は最終層の分類器を当問題のために学習させるのは feature extraction
と変わりませんが、事前に学習されたネットワーク層を「そのまま使いません」。
事前に巨大なデータセットで学習された「いくつかの層も学習」させるようにします。
PyTorchの転移学習チュートリアルにも2つの手法について、簡潔に説明が述べられています。
この例では、Resnet18
を使った feature extraction
(特徴抽出) を行います。
前提
- データを格納するS3バケットがあること
- Jupyterノートブックが作成されていること
- IAM権限を設定・更新できること
こちらの作業については下記を参照下さい。
はじめてのSageMaker みんな大好きアイリスデータを使って組み込みアルゴリズムで分類してみる
やること
10種類の画像を学習させて分類できるようにしようぜ! が趣旨です。
- データのロード、準備、S3アップロード
- モデルでの学習
- モデルのデプロイ
- モデルの検証
- 後片付け
1. データのロード、準備、S3アップロード
なお、PyTorchを使うNotebookはpytorchを選びましょう。
(torch
がない!?なぜだ!?となったタイミングがありました)
必要なライブラリとデータのロード
import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms, models
S3バケットにデータをアップロードするための準備です。
%%time import sagemaker import os import boto3 import re import numpy as np sagemaker_session = sagemaker.Session() role = sagemaker.get_execution_role() region = boto3.Session().region_name bucket='YOUR_BUCKET_NAME' prefix = 'sagemaker/cnn-cifar10' # customize to your bucket where you have stored the data bucket_path = 'https://s3-{}.amazonaws.com/{}'.format(region,bucket)
ここらへんはPyTorchのチュートリルに沿っていますが、
データ形式および正規化を施したローカルにDownloadされたデータを訓練データ、テストデータにセットします。
%%time transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) train_data = datasets.CIFAR10(root='../data', train=True, download=True, transform=transform) test_data = datasets.CIFAR10(root='../data', train=False, download=True, transform=transform)
S3バケットにデータをアップロードします。
%%time inputs = sagemaker_session.upload_data(path='../data', bucket=bucket, key_prefix=prefix) print('input spec (in this case, just an S3 path): {}'.format(inputs))
2. モデルでの学習
カスタム PyTorch コードを利用するために下記を参考にPythonスクリプトを作成します。
# feature_extract_cifar10.py
import argparse import json import sagemaker_containers import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms, models import torch import torchvision import torchvision.transforms as transforms import torch.optim as optim import logging import os import sys import time logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler(sys.stdout)) def _train(args): transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) train_data = datasets.CIFAR10( root='../data', train=True, download=True, transform=transform) test_data = datasets.CIFAR10( root='../data', train=False, download=True, transform=transform) torch.manual_seed(42) # for reproducible results train_loader = DataLoader(train_data, batch_size=100, shuffle=True) test_loader = DataLoader(test_data, batch_size=100, shuffle=False) logger.debug("Processes {}/{} ({:.0f}%) of train data".format( len(train_loader), len(train_loader.dataset), 100. * len(train_loader) / len(train_loader.dataset) )) logger.debug("Processes {}/{} ({:.0f}%) of test data".format( len(test_loader), len(test_loader.dataset), 100. * len(test_loader) / len(test_loader.dataset) )) model = models.resnet18(pretrained=True) for param in model.parameters(): param.requires_grad = False # Parameters of newly constructed modules have requires_grad=True by default num_ftrs = model.fc.in_features model.fc = nn.Sequential(nn.Linear(num_ftrs, 1024), nn.ReLU(), nn.Dropout(0.4), nn.Linear(1024, 10), nn.LogSoftmax(dim=1)) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum) epochs = args.epochs batch_size = args.batch_size since = time.time() for epoch in range(epochs): print('Epoch {}/{}'.format(epoch, epochs - 1)) print('-' * 12) running_loss = 0.0 running_corrects = 0 for i, (X_train, y_train) in enumerate(train_loader): i += 1 optimizer.zero_grad() y_pred = model(X_train) _, preds = torch.max(y_pred, 1) loss = criterion(y_pred, y_train) loss.backward() optimizer.step() running_loss += loss.item() * X_train.size(0) running_corrects += torch.sum(preds == y_train) # scheduler.step() epoch_loss = running_loss / (batch_size*i) epoch_acc = running_corrects.double() * 100/(batch_size*i) # statistics if i % 100 == 0: print( f'Loss: {epoch_loss:4f} batch: {i:4} [{batch_size*i:6}/50000] Accuracy: {epoch_acc:7.3f}%') time_elapsed = time.time() - since print('Training complete in {:.0f}m {:.0f}s'.format( time_elapsed // 60, time_elapsed % 60)) return _save_model(model, args.model_dir) def model_fn(model_dir): device = "cuda" if torch.cuda.is_available() else "cpu" model = models.resnet18(pretrained=True) for param in model.parameters(): param.requires_grad = False # Parameters of newly constructed modules have requires_grad=True by default num_ftrs = model.fc.in_features model.fc = nn.Sequential(nn.Linear(num_ftrs, 1024), nn.ReLU(), nn.Dropout(0.4), nn.Linear(1024, 10), nn.LogSoftmax(dim=1)) if torch.cuda.device_count() > 1: model = nn.DataParallel(model) with open(os.path.join(model_dir, 'model.pth'), 'rb') as f: model.load_state_dict(torch.load(f)) return model.to(device) def _save_model(model, model_dir): path = os.path.join(model_dir, 'model.pth') # recommended way from http://pytorch.org/docs/master/notes/serialization.html torch.save(model.cpu().state_dict(), path) if __name__ == '__main__': parser = argparse.ArgumentParser() # Data and model checkpoints directories 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=10, metavar='N', help='number of epochs to train (default: 10)') parser.add_argument('--lr', type=float, default=0.01, metavar='LR', help='learning rate (default: 0.01)') parser.add_argument('--momentum', type=float, default=0.5, metavar='M', help='SGD momentum (default: 0.5)') parser.add_argument('--seed', type=int, default=1, metavar='S', help='random seed (default: 1)') parser.add_argument('--log-interval', type=int, default=100, metavar='N', help='how many batches to wait before logging training status') parser.add_argument('--backend', type=str, default=None, help='backend for distributed training (tcp, gloo on cpu and gloo, nccl on gpu)') # Container environment parser.add_argument('--hosts', type=list, default=json.loads(os.environ['SM_HOSTS'])) parser.add_argument('--current-host', type=str, default=os.environ['SM_CURRENT_HOST']) parser.add_argument('--model-dir', type=str, default=os.environ['SM_MODEL_DIR']) parser.add_argument('--data-dir', type=str, default=os.environ['SM_CHANNEL_TRAINING']) parser.add_argument('--num-gpus', type=int, default=os.environ['SM_NUM_GPUS']) _train(parser.parse_args())
基本的にやっていることは、PyTorchで実行する内容をバラバラに関数として定義して、ハイパーパラメータを引数で取得できるようにします。
こちらも参考になりました。ありがとうございました。
AWS SageMakerでの機械学習モデル開発フロー(PyTorch)
Kerasを使ったユースケースは、弊社記事も参考になります。
Amazon SageMakerのScript modeを使ってKerasを動かしてみた #reinvent
ハイパーパラメータをそれぞれ設定して、モデルの訓練を実行します。
from sagemaker.pytorch import PyTorch hyper_param = { 'epochs':100, 'batch-size': 100, 'lr': 0.01, 'momentum': 0.9, } estimator = PyTorch(entry_point='feature_extract_cifar10.py', hyperparameters=hyper_param, role=role, framework_version='1.2.0', train_instance_count=2, train_instance_type='ml.c5.xlarge')
estimator.fit({'training': inputs}, logs=True)
なお、現在(2020/4/24)、 framework_version
が1.3および1.4だと
AlgorithmError: ExecuteUserScriptError: Command
エラーが出て実行できませんでした。
ちなみに、ローカルPCで転移学習しない5層程度のネットワークモデルの訓練データの正解率は、
50epochで85%くらい までいきました。(3,40分)
検証データの正解率は65~70%でした。
こいつあ、きっと少なくとも訓練データの正解率90%超は間違いないよな!?
...
...
1時間以上経過
...
え、ええーー。。
ショックで何これ?とうなだれていると、社内で教えて頂きます。
なるほど。。
どうも torchvision
のデータセットが元の写真サイズとは異なるためらしい。
初歩的だあ。。(今ちょっとやり直す元気もうない。)
3. モデルのデプロイ
まぁ結果はどうあれ、とりあえずやりたいことはできたからいいや、ということでエンドポイントを作成して
predictor = estimator.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')
4. モデルの検証
10,000のテストデータを使って正解率を確認してみます。
import numpy as np correct = 0 total = 0 with torch.no_grad(): for data in test_loader: images, labels = data outputs = predictor.predict(images.numpy()) _, predicted = torch.max(torch.from_numpy(np.array(outputs)), 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the network on the 10000 test images: %d %%' % ( 100 * correct / total))
50% という惨憺たる結果でした。。
5. 後片付け
忘れずにエンドポイントは削除します。
削除しないとお金が発生しますのでご注意下さい。
estimator.delete_endpoint()
インスタンスも不用であれば削除しちゃいましょう。
S3バケットの存在も忘れずに。
最後に
結果をスピーディーに出していくためには一連の作業を効率化、ハイパーパラメータの調整手法を学ぶ必要があるなと思いました。
ハイパーパラメータの値で結構な差が出る、恐ろしい、、、ということも学びました。
(逆に言えば、何やってるのか全く分からなくてもハイパーパラメータさえうまく調整できればある程度結果は出せるのだろうと思います。)
GPUって高いけど速いんだなーーということも分かりました。
以上です。
引き続き学習を進めていきます。
どなたかのお役に立てば幸いです。
参考:
- Transfer learning
- TRAINING A CLASSIFIER
- TRANSFER LEARNING FOR COMPUTER VISION TUTORIAL
- PyTorch によるモデルのトレーニング
- awslabs/amazon-sagemaker-examples
- AWS SageMakerでの機械学習モデル開発フロー(PyTorch)