728x90

목차

     

    관련글

    saved_model 배포 방법을 한 번 시도해보고 읽어보길 권장

    https://jfbta.tistory.com/320

     

    [머신러닝] 쿠버네티스에서 TensorFlow 모델 Triton 서버를 활용해서 서빙하기(saved_model)

    목차 쿠버네티스에서 트리톤 이미지 파드로 띄우기kubectl create -f triton-pvc.yamlkubectl create -f triton-deployment.yaml```triton-pvc.yamlapiVersion: v1kind: PersistentVolumeClaimmetadata: name: triton-pvc namespace: ${네임스페이

    jfbta.tistory.com

     

    onnx 모델 생성하기

     

    import torch
    import numpy as np
    from torchvision.datasets import ImageFolder
    import torchvision.transforms as transforms
    from torch.utils.data import Dataset, DataLoader
    from torch.utils.data import random_split
    import os
    from PIL import Image
    
    class CustomDataset(Dataset):
        def __init__(self, root_dir, transform=None):
            self.root_dir = root_dir
            self.transform = transform
            self.images = []
            self.labels = []
    
            # 이미지 파일 경로와 레이블 수집
            for filename in os.listdir(root_dir):
                if filename.endswith(".jpg"):  # JPEG 이미지 파일만 포함
                    label = 0 if 'cat' in filename else 1  # 'cat'이면 0, 'dog'이면 1
                    self.images.append(os.path.join(root_dir, filename))
                    self.labels.append(label)
    
        def __len__(self):
            return len(self.images)
    
        def __getitem__(self, idx):
            img_path = self.images[idx]
            image = Image.open(img_path).convert("RGB")  # 이미지를 RGB 모드로 열기
            label = self.labels[idx]
    
            if self.transform:
                image = self.transform(image)
    
            return image, label
        
    train_transforms = transforms.Compose([transforms.RandomRotation(30), # 랜덤 각도 회전
                                           transforms.RandomResizedCrop(224), # 랜덤 리사이즈 크롭
                                           transforms.RandomHorizontalFlip(), # 랜덤으로 수평 뒤집기
                                           transforms.ToTensor(), # 이미지를 텐서로
                                           transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]) #
    
    
    
    dataset = CustomDataset(root_dir='C:/Users/Downloads/modelapi/model/onnx_model/data/train/test', transform=train_transforms) # 파일 디렉토리
    
    trainset, validset = random_split(dataset, [3,1]) # 학습데이터를 학습과 검증데이터로 쪼갬
    
    batch_size=10
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
    validloader = torch.utils.data.DataLoader(validset, batch_size=batch_size, shuffle=True)
    classes = ['cat', 'dog']
    
    # 코드 작성
    import torch.nn as nn
    import torch.nn.functional as F
    
    # 좀 더 심플한 네트워크
    class Net(nn.Module):
        def __init__(self):
            super(Net, self).__init__()
    
            # input image = 224 x 224 x 3
    
            # 224 x 224 x 3 --> 112 x 112 x 32 maxpool
            self.conv1 = nn.Conv2d(3, 32, 3, padding=1) 
            # 112 x 112 x 32 --> 56 x 56 x 64 maxpool
            self.conv2 = nn.Conv2d(32, 64, 3, padding=1) 
            # 56 x 56 x 64 --> 28 x 28 x 128 maxpool
            self.conv3 = nn.Conv2d(64, 128, 3, padding=1)    
    
            # maxpool 2 x 2
            self.pool = nn.MaxPool2d(2, 2)
    
            # 28 x 28 x 128 vector flat 256개
            self.fc1 = nn.Linear(128 * 28 * 28, 256)
            # 카테고리 2개 클래스
            self.fc2 = nn.Linear(256, 2) 
    
            # dropout 적용
            self.dropout = nn.Dropout(0.5)
    
        def forward(self, x):
            # conv1 레이어에 relu 후 maxpool. 112 x 112 x 32
            x = self.pool(F.relu(self.conv1(x)))
            # conv2 레이어에 relu 후 maxpool. 56 x 56 x 64
            x = self.pool(F.relu(self.conv2(x)))
            # conv3 레이어에 relu 후 maxpool. 28 x 28 x 128
            x = self.pool(F.relu(self.conv3(x)))
    
            # 이미지 펴기
            x = x.view(-1, 128 * 28 * 28)
            # dropout 적용
            x = self.dropout(x)
            # fc 레이어에 삽입 후 relu
            x = F.relu(self.fc1(x))
            # dropout 적용
            x = self.dropout(x)
    
            # 마지막 logsoftmax 적용
            x = F.log_softmax(self.fc2(x), dim=1)
            return x
    
    
    model = Net() # 모델 생성
    print(model) # 출력
    
    
    # 코드
    import torch.optim as optim
    
    criterion = nn.NLLLoss()
    
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    
    
    # 코드 작성
    
    # epochs 30
    n_epochs = 10
    
    valid_loss_min = np.Inf
    
    for epoch in range(1, n_epochs+1):
        # train, valid loss
        print(epoch)
        train_loss = 0.0
        valid_loss = 0.0
        
        # 모델 트레이닝
        model.train()
        # training set
        for batch_idx, (data, target) in enumerate(trainloader, 1):
            # cuda 사용
            # 역전파 실행 전 gradient 0 초기화
            optimizer.zero_grad()
            # 모델 계산 후 output 저장
            output = model(data)
            # 로스율 계산
            loss = criterion(output, target)
            # 가중치 계산
            loss.backward()
            # 모델 parameter 업데이트
            optimizer.step()
            # 트레이닝 로스 계산
            train_loss += loss.item()*data.size(0)
            # 배치마다 트레이닝 손실 출력
            if batch_idx % 10 == 0:  # 예: 10번째 배치마다 출력
                print(f"  Batch {batch_idx}, Loss: {loss.item():.6f}")
    
        # validation 모델
        model.eval()
        validation_iter = iter(validloader)
        with torch.no_grad():  # validation 시 gradient 계산 안 함
            for data, target in validation_iter:
                # cuda 사용
                # 모델 계산 후 output 저장
                output = model(data)
                # 로스율 계산
                loss = criterion(output, target)
                # validation 로스율 계산
                valid_loss += loss.item()*data.size(0)
    
        # 평균 로스율
        train_loss = train_loss/len(trainloader.sampler)
        valid_loss = valid_loss/len(validloader.sampler)
    
        # training set, validation set 로스율 출력
        print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format(
            epoch, train_loss, valid_loss))
        
        # 로스율이 낮아지면 model_catdog.pt에 저장
        if valid_loss <= valid_loss_min:
            print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(
            valid_loss_min,
            valid_loss))
            torch.save(model.state_dict(), 'model_catdog.pt')
            valid_loss_min = valid_loss
        
    
    # 최종적으로 나온 모델을 onnx 파일로 저장    
    import onnx
    dummy_input = torch.randn(1, 3, 224, 224)  # 입력 크기에 맞게 조정
    
    torch.onnx.export(model, dummy_input, "cat_dog_model.onnx", 
                      input_names=["input_1"], 
                      output_names=["output_1"],
                      opset_version=11,
                      dynamic_axes={"input_1": {0: "batch_size"}, "output_1": {0: "batch_size"}})
    
    # torch.onnx.export(model, dummy_input, "cat_dog_model.onnx", 
    #                   input_names=["input_1"], output_names=["output_1"],
    #                   opset_version=11)

    'dataset = CustomDataset(root_dir='C:/Users/Downloads/modelapi/model/onnx_model/data/train/test', transform=train_transforms)' 이 경로에 있는 많은 양의 강아지, 고양이 이미지를 학습시키는 머신러닝 코드이다. 실행이 완료되는데 반나절 정도의 시간이 걸리며 완료되면 model.onnx 파일이 생성된다.

     

    트리톤 서버에서 모델 디렉토리 Tree 형식으로 구조 파악하고 구성하기


    ```디렉토리 구조
    /models
      /onnx_model
        /config.pbtxt
        1
          /model.onnx
    ```

     

    config.pbtxt 작성하기

    name: "onnx_model"
    platform: "onnxruntime_onnx"
    max_batch_size: 1
    input [
      {
        name: "input_1"
        data_type: TYPE_FP32
        format: FORMAT_NCHW
        dims: [ 3, 224, 224 ]
      }
    ]
    output [
      {
        name: "output_1"
        data_type: TYPE_FP32
        dims: [ 2 ]
      }
    ]
    dynamic_batching {
      preferred_batch_size: [ 1 ]
      max_queue_delay_microseconds: 100
    }

     

    이미지 크기를 224*224로 했더니 이미지를 json으로 변환해서 body에 입력하여 post api 호출했더니 다음과 같은 에러가 발생했다.

    요청 실패: 413, <html>
    <head><title>413 Request Entity Too Large</title></head>
    <body>
    <center><h1>413 Request Entity Too Large</h1></center>
    <hr><center>nginx</center>
    </body>
    </html>

    '<center>nginx</center>' 이 부분을 보니 ingress-nginx에서 size를 늘리는 설정이 필요한 것 같다.

     

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/proxy-body-size: 100m
        nginx.ingress.kubernetes.io/rewrite-target: /$2
      creationTimestamp: "2024-09-23T06:30:26Z"
      generation: 1
      name: triton-ingress-21
      namespace: "2"
      resourceVersion: "52473063"
      uid: 18374b2c-2759-49ea-b344-de5552b16951
    spec:
      ingressClassName: nginx
      rules:
      - http:
          paths:
          - backend:
              service:
                name: triton-svc-21
                port:
                  number: 8000
            path: /21(/|$)(.*)
            pathType: Prefix
    status:
      loadBalancer:
        ingress:
        - ip: 10.10.12.123

    'nginx.ingress.kubernetes.io/proxy-body-size: 100m' -> nginx에서 body size의 기본값은 1m 이다. 100m 까지 늘려준뒤 호출 해주면 정상적으로 동작된다.

     

    이미지를 triton body 입력 형식에 맞는 json타입으로 변환 후 POST API 호출하여 - 고양이, 강아지 예측하기

    import requests
    import json
    import numpy as np
    from PIL import Image
    import torchvision.transforms as transforms
    
    # 입력데이터 전처리
    def createInputData(image_path):
        input_image = Image.open(image_path)
        preprocess = transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        ])
        input_tensor = preprocess(input_image).unsqueeze(0)  # 배치 차원 추가
        input_data_flat = input_tensor.numpy().flatten().tolist()
    
        # JSON 형식으로 작성
        request_json = {
            "inputs": [
                {
                    "name": "input_1",  # 모델의 입력 이름
                    "shape": [1, 3, 224, 224],  # 배치 크기를 포함한 입력 형태
                    "datatype": "FP32",  # 데이터 타입
                    "data": input_data_flat  # 플래트 형식의 데이터 배열
                }
            ]
        }
        return request_json
        
    # 응답 처리
    def callAPI(input_data):
        response = requests.post(url, json=input_data)
        if response.status_code == 200:
            outputs = response.json()  # JSON 응답 파싱
            data_values = outputs['outputs'][0]['data']
            print(outputs)
            class_names = ['고양이', '강아지']
            result=class_names[np.argmax(data_values)]
            print(f"예측 결과: {result}")
        else:
            print(f"요청 실패: {response.status_code}, {response.text}")
        return requests.post(url, json=input_data)
    image_path = 'cat.jpg'
    input_data = createInputData(image_path)
    # Triton API 호출을 위한 URL 설정
    url = "http://10.10.12.123/21/v2/models/onnx_model/infer"
    # API 호출 - 완전한 JSON 객체 전송
    callAPI(input_data)
    예측 결과: 고양이
    728x90
    TOP