

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 延伸預先建置的容器
<a name="prebuilt-containers-extend"></a>

如果預先建置的 SageMaker AI 容器無法滿足所有需求，您可以延伸現有映像以因應您的需求。即使為您的環境或架構提供直接支援，您可能想要新增其他功能，或以不同方式設定容器環境。透過延伸預先建置的映像，您可以利用內含的深度學習程式庫和設定，無需從頭開始建立映像。您可以延伸容器，以新增程式庫、修改設定和安裝其他相依性。

下列教學課程說明如何延伸預先建置的 SageMaker 映像，並將其發佈到 Amazon ECR。

**Topics**
+ [延伸預先建置容器的需求](#prebuilt-containers-extend-required)
+ [延伸 SageMaker AI 容器來執行 Python 指令碼](#prebuilt-containers-extend-tutorial)

## 延伸預先建置容器的需求
<a name="prebuilt-containers-extend-required"></a>

若要延伸預先建置的 SageMaker 映像，您需要在 Dockerfile 中設定下列環境變數。如需使用 SageMaker AI 容器的環境變數的更多詳細資訊，請參閱 [SageMaker Training Toolkit GitHub 儲存庫](https://github.com/aws/sagemaker-training-toolkit/blob/master/ENVIRONMENT_VARIABLES.md)。
+ `SAGEMAKER_SUBMIT_DIRECTORY`：容器中用於訓練的 Python 指令碼所在的目錄。
+ `SAGEMAKER_PROGRAM`：應調用並用作訓練進入點的 Python 指令碼。

您還可以透過在 Dockerfile 中包含下列內容來安裝其他程式庫：

```
RUN pip install <library>
```

下列教學課程將介紹如何使用這些環境變數。

## 延伸 SageMaker AI 容器來執行 Python 指令碼
<a name="prebuilt-containers-extend-tutorial"></a>

在此教學課程中，您將了解如何透過使用 CIFAR-10 資料集的 Python 檔案來延伸 SageMaker AI PyTorch 容器。透過延伸 SageMaker AI PyTorch 容器，您利用專門與 SageMaker AI 搭配使用的現有訓練解決方案。本教學課程延伸訓練映像，但也可以採取相同步驟來延伸推論映像。有關可用映像的完整清單，請參閱[可用的深度學習容器映像](https://github.com/aws/deep-learning-containers/blob/master/available_images.md)。

若要使用 SageMaker AI 容器執行您自己的訓練模型，請透過 SageMaker 筆記本執行個體建置 Docker 容器。

### 步驟 1：建立一個 SageMaker 筆記本執行個體
<a name="extend-step1"></a>

1. 開啟 [SageMaker AI 主控台](https://console.aws.amazon.com/sagemaker/)。

1. 從左邊導覽窗格中，選擇**筆記本**，選擇**筆記本執行個體**，然後選擇**建立筆記本執行個體**。

1. 在**建立筆記本執行個體**頁面上，提供下列資訊：

   1. 對於**筆記本執行個體名稱**，輸入 **RunScriptNotebookInstance**。

   1. 對於**筆記本執行個體類型**，選擇 **ml.t2.medium**。

   1. 在**許可與加密**區段內執行下列動作：

      1. 對於 **IAM 角色**，選擇**建立新角色**。

      1. 在**建立 IAM 角色**頁面上，選擇**特定的 S3 儲存貯體**，指定名為 **sagemaker-run-script** 的 Amazon S3 儲存貯體，然後選擇**建立角色**。

         SageMaker AI 會建立名為 `AmazonSageMaker-ExecutionRole-YYYYMMDDTHHmmSS` 的 IAM 角色，例如 `AmazonSageMaker-ExecutionRole-20190429T110788`。請注意，執行角色命名慣例會使用角色建立時的日期和時間，並以 `T` 分隔。

   1. 對於**根存取**，選擇**已啟用**。

   1. 選擇**建立筆記本執行個體**。

1. 在**筆記本執行個體**頁面上，**狀態**為**待定**。Amazon SageMaker AI 可能需要幾分鐘的時間才能啟動機器學習運算執行個體 (在此情況下，它會啟動筆記本執行個體)，並將機器學習 (ML) 儲存磁碟區連接到執行個體上。筆記本執行個體具備預先設定的 Jupyter 筆記本伺服器和一組 Anaconda 程式庫。如需詳細資訊，請參閱 [CreateNotebookInstance](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateNotebookInstance.html)。

   

1. 在**許可和加密**區段中，複製 **IAM 角色 ARN 編號**，然後將其貼到記事本檔案中以暫時儲存。稍後您可以使用此 IAM 角色 ARN 編號，在筆記本執行個體中設定本機訓練估算器。**IAM 角色 ARN 編號**如下所示：`'arn:aws:iam::111122223333:role/service-role/AmazonSageMaker-ExecutionRole-20190429T110788'`

1. 筆記本執行個體的狀態變更為 **InService** 後，請選擇**開啟 JupyterLab**。

### 步驟 2：建立並上傳 Dockerfile 和 Python 訓練指令碼
<a name="extend-step2"></a>

1. 開啟 JupyterLab 後，在 JupyterLab 主目錄內建立一個新資料夾。在左上角選擇**新增資料夾**圖示，然後輸入資料夾名稱 `docker_test_folder`。

1.  在 `docker_test_folder` 目錄中，建立一個 `Dockerfile` 文字檔案。

   1. 選擇左上角的**新增啟動器**圖示 (\$1)。

   1. 在**其他**區段下右邊的窗格中，選擇**文字檔案**。

   1.  將下列 `Dockerfile` 範例程式碼貼到您的文字檔中。

      ```
      # SageMaker PyTorch image
      FROM 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04
      
      ENV PATH="/opt/ml/code:${PATH}"
      
      # this environment variable is used by the SageMaker PyTorch container to determine our user code directory.
      ENV SAGEMAKER_SUBMIT_DIRECTORY /opt/ml/code
      
      # /opt/ml and all subdirectories are utilized by SageMaker, use the /code subdirectory to store your user code.
      COPY cifar10.py /opt/ml/code/cifar10.py
      
      # Defines cifar10.py as script entrypoint 
      ENV SAGEMAKER_PROGRAM cifar10.py
      ```

      Dockerfile 指令碼會執行以下任務：
      + `FROM 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04` – 下載 SageMaker AI PyTorch 基礎映像。您可以將此項目取代為您要帶來建置容器的任何 SageMaker AI 基礎映像。
      + `ENV SAGEMAKER_SUBMIT_DIRECTORY /opt/ml/code` – 將 `/opt/ml/code` 設定為訓練指令碼目錄。
      + `COPY cifar10.py /opt/ml/code/cifar10.py` – 將指令碼複製到 SageMaker AI 在容器內預期的位置。此指令碼必須位於此資料夾。
      + `ENV SAGEMAKER_PROGRAM cifar10.py` – 將您的 `cifar10.py` 訓練指令碼設定為進入點指令碼。

   1.  在左側目錄導覽窗格中，文字檔案名稱可能自動命名為 `untitled.txt`。若要重新命名檔案，請在該檔案上按一下滑鼠右鍵，選擇**重新命名**，將檔案重新命名為 `Dockerfile` (不包含 `.txt` 副檔名)，然後按下 `Ctrl+s` 或 `Command+s` 以儲存檔案。

1. 在 `docker_test_folder` 中建立或上傳訓練指令碼 `cifar10.py`。您可以使用下列範例指令碼進行這個練習。

   ```
   import ast
   import argparse
   import logging
   
   import os
   
   import torch
   import torch.distributed as dist
   import torch.nn as nn
   import torch.nn.parallel
   import torch.optim
   import torch.utils.data
   import torch.utils.data.distributed
   import torchvision
   import torchvision.models
   import torchvision.transforms as transforms
   import torch.nn.functional as F
   
   logger=logging.getLogger(__name__)
   logger.setLevel(logging.DEBUG)
   
   classes=('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
   
   
   # https://github.com/pytorch/tutorials/blob/master/beginner_source/blitz/cifar10_tutorial.py#L118
   class Net(nn.Module):
       def __init__(self):
           super(Net, self).__init__()
           self.conv1=nn.Conv2d(3, 6, 5)
           self.pool=nn.MaxPool2d(2, 2)
           self.conv2=nn.Conv2d(6, 16, 5)
           self.fc1=nn.Linear(16 * 5 * 5, 120)
           self.fc2=nn.Linear(120, 84)
           self.fc3=nn.Linear(84, 10)
   
       def forward(self, x):
           x=self.pool(F.relu(self.conv1(x)))
           x=self.pool(F.relu(self.conv2(x)))
           x=x.view(-1, 16 * 5 * 5)
           x=F.relu(self.fc1(x))
           x=F.relu(self.fc2(x))
           x=self.fc3(x)
           return x
   
   
   def _train(args):
       is_distributed=len(args.hosts) > 1 and args.dist_backend is not None
       logger.debug("Distributed training - {}".format(is_distributed))
   
       if is_distributed:
           # Initialize the distributed environment.
           world_size=len(args.hosts)
           os.environ['WORLD_SIZE']=str(world_size)
           host_rank=args.hosts.index(args.current_host)
           dist.init_process_group(backend=args.dist_backend, rank=host_rank, world_size=world_size)
           logger.info(
               'Initialized the distributed environment: \'{}\' backend on {} nodes. '.format(
                   args.dist_backend,
                   dist.get_world_size()) + 'Current host rank is {}. Using cuda: {}. Number of gpus: {}'.format(
                   dist.get_rank(), torch.cuda.is_available(), args.num_gpus))
   
       device='cuda' if torch.cuda.is_available() else 'cpu'
       logger.info("Device Type: {}".format(device))
   
       logger.info("Loading Cifar10 dataset")
       transform=transforms.Compose(
           [transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
   
       trainset=torchvision.datasets.CIFAR10(root=args.data_dir, train=True,
                                               download=False, transform=transform)
       train_loader=torch.utils.data.DataLoader(trainset, batch_size=args.batch_size,
                                                  shuffle=True, num_workers=args.workers)
   
       testset=torchvision.datasets.CIFAR10(root=args.data_dir, train=False,
                                              download=False, transform=transform)
       test_loader=torch.utils.data.DataLoader(testset, batch_size=args.batch_size,
                                                 shuffle=False, num_workers=args.workers)
   
       logger.info("Model loaded")
       model=Net()
   
       if torch.cuda.device_count() > 1:
           logger.info("Gpu count: {}".format(torch.cuda.device_count()))
           model=nn.DataParallel(model)
   
       model=model.to(device)
   
       criterion=nn.CrossEntropyLoss().to(device)
       optimizer=torch.optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
   
       for epoch in range(0, args.epochs):
           running_loss=0.0
           for i, data in enumerate(train_loader):
               # get the inputs
               inputs, labels=data
               inputs, labels=inputs.to(device), labels.to(device)
   
               # zero the parameter gradients
               optimizer.zero_grad()
   
               # forward + backward + optimize
               outputs=model(inputs)
               loss=criterion(outputs, labels)
               loss.backward()
               optimizer.step()
   
               # print statistics
               running_loss += loss.item()
               if i % 2000 == 1999:  # print every 2000 mini-batches
                   print('[%d, %5d] loss: %.3f' %
                         (epoch + 1, i + 1, running_loss / 2000))
                   running_loss=0.0
       print('Finished Training')
       return _save_model(model, args.model_dir)
   
   
   def _save_model(model, model_dir):
       logger.info("Saving the model.")
       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)
   
   
   def model_fn(model_dir):
       logger.info('model_fn')
       device="cuda" if torch.cuda.is_available() else "cpu"
       model=Net()
       if torch.cuda.device_count() > 1:
           logger.info("Gpu count: {}".format(torch.cuda.device_count()))
           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)
   
   
   if __name__ == '__main__':
       parser=argparse.ArgumentParser()
   
       parser.add_argument('--workers', type=int, default=2, metavar='W',
                           help='number of data loading workers (default: 2)')
       parser.add_argument('--epochs', type=int, default=2, metavar='E',
                           help='number of total epochs to run (default: 2)')
       parser.add_argument('--batch-size', type=int, default=4, metavar='BS',
                           help='batch size (default: 4)')
       parser.add_argument('--lr', type=float, default=0.001, metavar='LR',
                           help='initial learning rate (default: 0.001)')
       parser.add_argument('--momentum', type=float, default=0.9, metavar='M', help='momentum (default: 0.9)')
       parser.add_argument('--dist-backend', type=str, default='gloo', help='distributed backend (default: gloo)')
   
       # The parameters below retrieve their default values from SageMaker environment variables, which are
       # instantiated by the SageMaker containers framework.
       # https://github.com/aws/sagemaker-containers#how-a-script-is-executed-inside-the-container
       parser.add_argument('--hosts', type=str, default=ast.literal_eval(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())
   ```

### 步驟 3：建置容器
<a name="extend-step3"></a>

1. 在 JupyterLab 主目錄中，開啟 Jupyter 筆記本。若要開啟新筆記本，請選擇**新的啟動**圖示，然後在**筆記本**區段中選擇 **conda\$1pytorch\$1p39**。

1. 在第一個筆記本儲存格中執行下列命令，以切換至 `docker_test_folder` 目錄：

   ```
   % cd ~/SageMaker/docker_test_folder
   ```

   這樣會返回目前的目錄，如下所示：

   ```
   ! pwd
   ```

   `output: /home/ec2-user/SageMaker/docker_test_folder`

1. 登入 Docker 以存取基礎容器：

   ```
   ! aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 763104351884.dkr.ecr.us-east-1.amazonaws.com
   ```

1. 若要建置 Docker 容器，請執行下列 Docker 建置命令，包含在結尾處有句點的空格：

   ```
   ! docker build -t pytorch-extended-container-test .
   ```

   必須從您建立的 Docker 目錄中執行 Docker build 命令，在此案例中為 `docker_test_folder`。
**注意**  
如果您收到以下錯誤訊息，表示 Docker 找不到 Dockerfile，請確認 Dockerfile 的名稱正確，且已存入目錄。  

   ```
   unable to prepare context: unable to evaluate symlinks in Dockerfile path: 
   lstat /home/ec2-user/SageMaker/docker/Dockerfile: no such file or directory
   ```
請記住，`docker` 會在當前目錄中查找名稱為 `Dockerfile` 且不含任何副檔名的檔案。如果您將其命名為其他名稱，則可以使用 `-f` 標記手動輸入檔案名稱。例如，如果您將 Docerfile 命名為 `Dockerfile-text.txt`，請執行下列命令：  

   ```
   ! docker build -t tf-custom-container-test -f Dockerfile-text.txt .
   ```

### 步驟 4：測試容器
<a name="extend-step4"></a>

1. 若要在筆記本執行個體內本機測試容器，請開啟 Jupyter 筆記本。選擇**新啟動器**，然後在 **`conda_pytorch_p39`** 架構中選擇**筆記本**。其餘的程式碼片段必須從 Jupyter 筆記本執行個體中執行。

1. 下載 CIFAR-10 資料集。

   ```
   import torch
   import torchvision
   import torchvision.transforms as transforms
   
   def _get_transform():
       return transforms.Compose(
           [transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
   
   
   def get_train_data_loader(data_dir='/tmp/pytorch/cifar-10-data'):
       transform=_get_transform()
       trainset=torchvision.datasets.CIFAR10(root=data_dir, train=True,
                                               download=True, transform=transform)
       return torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)
   
   
   def get_test_data_loader(data_dir='/tmp/pytorch/cifar-10-data'):
       transform=_get_transform()
       testset=torchvision.datasets.CIFAR10(root=data_dir, train=False,
                                              download=True, transform=transform)
       return torch.utils.data.DataLoader(testset, batch_size=4,
                                          shuffle=False, num_workers=2)
   
   trainloader=get_train_data_loader('/tmp/pytorch-example/cifar-10-data')
   testloader=get_test_data_loader('/tmp/pytorch-example/cifar-10-data')
   ```

1. 將 `role` 設定為用於建立 Jupyter 筆記本的角色。這是用於配置您的 SageMaker AI 估算器。

   ```
   from sagemaker import get_execution_role
   
   role=get_execution_role()
   ```

1. 將下列範例指令碼貼到筆記本程式碼儲存格中，以使用延伸容器來配置 SageMaker AI 估算器。

   ```
   from sagemaker.estimator import Estimator
   
   hyperparameters={'epochs': 1}
   
   estimator=Estimator(
       image_uri='pytorch-extended-container-test',
       role=role,
       instance_count=1,
       instance_type='local',
       hyperparameters=hyperparameters
   )
   
   estimator.fit('file:///tmp/pytorch-example/cifar-10-data')
   ```

1. 執行程式碼儲存格。此測試會輸出訓練環境組態、用於環境變數的值、資料的來源，以及訓練期間獲得的損失和準確率。

### 步驟 5：將容器推送至 Amazon Elastic Container Registry (Amazon ECR)
<a name="extend-step5"></a>

1. 成功執行本機模式測試之後，您可以將 Docker 容器推送至 [Amazon ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html)，並使用它執行訓練工作。

   在筆記本儲存格中執行以下命令列。

   ```
   %%sh
   
   # Specify an algorithm name
   algorithm_name=pytorch-extended-container-test
   
   account=$(aws sts get-caller-identity --query Account --output text)
   
   # Get the region defined in the current configuration (default to us-west-2 if none defined)
   region=$(aws configure get region)
   
   fullname="${account}.dkr.ecr.${region}.amazonaws.com/${algorithm_name}:latest"
   
   # If the repository doesn't exist in ECR, create it.
   
   aws ecr describe-repositories --repository-names "${algorithm_name}" > /dev/null 2>&1
   if [ $? -ne 0 ]
   then
   aws ecr create-repository --repository-name "${algorithm_name}" > /dev/null
   fi
   
   # Log into Docker
   aws ecr get-login-password --region ${region}|docker login --username AWS --password-stdin ${fullname}
   
   # Build the docker image locally with the image name and then push it to ECR
   # with the full name.
   
   docker build -t ${algorithm_name} .
   docker tag ${algorithm_name} ${fullname}
   
   docker push ${fullname}
   ```

1. 推送容器之後，您可以從 SageMaker AI 環境中的任何位置呼叫 Amazon ECR 映像。在下一個筆記本儲存格中執行下列程式碼範例。

   如果您想要將此訓練容器與 SageMaker Studio 搭配使用，以使用其視覺化特徵，您也可以在 Studio 筆記本儲存格中執行下列程式碼，以呼叫訓練容器的 Amazon ECR 映像。

   ```
   import boto3
   
   client=boto3.client('sts')
   account=client.get_caller_identity()['Account']
   
   my_session=boto3.session.Session()
   region=my_session.region_name
   
   algorithm_name="pytorch-extended-container-test"
   ecr_image='{}.dkr.ecr.{}.amazonaws.com/{}:latest'.format(account, region, algorithm_name)
   
   ecr_image
   # This should return something like
   # 12-digits-of-your-account.dkr.ecr.us-east-2.amazonaws.com/tf-2.2-test:latest
   ```

1. 使用從上一個步驟擷取的 `ecr_image` 以設定 SageMaker AI 估算器物件。下列程式碼範例設定 SageMaker AI PyTorch 估算器。

   ```
   import sagemaker
   
   from sagemaker import get_execution_role
   from sagemaker.estimator import Estimator
   
   estimator=Estimator(
       image_uri=ecr_image,
       role=get_execution_role(),
       base_job_name='pytorch-extended-container-test',
       instance_count=1,
       instance_type='ml.p2.xlarge'
   )
   
   # start training
   estimator.fit()
   
   # deploy the trained model
   predictor=estimator.deploy(1, instance_type)
   ```

### 步驟 6：清除資源
<a name="extend-step6"></a>

**若要在入門範例使用完畢後清除資源**

1. 開啟 [SageMaker AI 主控台](https://console.aws.amazon.com/sagemaker/)，選取筆記本執行個體 **RunScriptNotebookInstance**，選擇**動作**，然後選擇**停止**。停止執行個體可能需要幾分鐘。

1. 執行個體**狀態**變更為**已停止**後，選擇**動作**，選擇**刪除**，然後在對話方塊中選擇**刪除**。刪除執行個體可能需要幾分鐘。當筆記本執行個體被刪除，會從表格中消失。

1. 開啟 [Amazon S3 主控台](https://console.aws.amazon.com/s3/)，刪除您為了儲存模型成品和訓練資料集而建立的儲存貯體。

1. 開啟 [IAM 主控台](https://console.aws.amazon.com/iam/)並刪除該 IAM 角色。如果已建立許可政策，也可一併刪除。
**注意**  
 Docker 容器執行之後會自動關閉。您不需要刪除它。