본문 바로가기

활동/SK네트웍스 Family AI 캠프 2기

SK네트웍스 Family AI 캠프 2기 : 14th week (8월 3주차)

출석률 50% 지점에 도달했습니다!

5월에 시작했던 프로그램인데 이렇게 빨리 시간이 흘렀다는 게 믿기지 않아요.

 

지금 제 모습을 첫날과 비교해 보니 꽤 많이 성장했다는 것이 느껴지더라구요.

그동안 코딩하면서 얻은 각종 삽질 경험들이 좋은 멘토님들을 만나 빛을 발하게 된 것 같습니다.

앞으로 어떤 길을 가면 좋을지 조금씩 눈이 트이는 기분이 들어요.

 

이 감각을 유지하면서 매일 정진해야겠어요.

자만하지 말고 겸손한 태도로 수양하겠습니다!

 

 

● 성취

이번 주 수업을 통해 제 관심사를 새로 발견하게 되었어요!

이전에 AI 모델을 다뤘을 때 추론 시간이 너무 느려서 가끔씩 당혹스러웠던 기억이 있거든요.

 

추론 시에도 학습 환경처럼 장치가 잘 세팅되어 있으면 큰 문제가 없었지만, 전혀 다른 공간에서 test를 해야 하는 순간이 생길 때 성능 면에서 변동이 심했어요.

 

이번에 배운 OpenVINO 라이브러리를 잘 활용하면 그동안 겪었던 문제들을 해결할 수 있을 것 같습니다!

OpenVINO 라이브러리를 사용하면 cpu 환경에서도 추론을 비교적 빠르고 안정적으로 할 수 있게 되거든요.

참고로 OpenVINO 라이브러리는 cpu로 유명한 Intel 사가 진행하고 있는 오픈 프로젝트라고 합니다.

아무래도 gpu로 수직 상승하고 있는 Nvidia 사를 견제하기 위한 목적성이 강해 보이네요!

 

AI를 주제로 여러 기업들이 저마다 생존 전략을 펼치고 있는 게 꼭 삼국지 시리즈를 떠올리게 만들어요.

 

이번 주 수업을 계기로 OpenVINO 라이브러리에 대해 더 알아볼 생각입니다!

작고 소소해 보일지 몰라도 제게는 큰 성취입니다.

 

꿈을 달성하는 것만큼 새로운 꿈을 꾸는 것 역시 중요한 것이라 생각하거든요!

특히 나이가 들고 성숙해질수록 후자에 대한 의욕이 시들해지는 경우가 많더라구요.

따라서 꿈을 꾸는 감각도 전자만큼 유지하는 것이 중요하다고 봐요.

 

Hold fast to dreams
For if dreams die
Life is a broken winged bird
That cannot fly.

- Langston Hughes (랭스턴 휴스) -

 

● 학습

이번 주에 Vision AI 모델에 대해 배웠습니다.

위에서 언급한 OpenVINO 라이브러리도 Vision 분야를 다루면서 배운 주제 중 하나입니다.

+) 물론 OpenVINO 라이브러리가 Vision만 처리 가능한 것은 아닙니다...^^ NLP 등에도 적용 가능해요!

 

오늘은 수업에서 OpenVINO 코드의 일부를 블로그 포스팅을 남겨보려고 합니다 ^_^

 

참고로 수업 시간에는 코랩 환경을 사용했습니다!

로컬 환경 경험이 여러모로 좋긴 하지만 디버깅 비용이 높아지긴 하더라구요.

이 문제를 가급적 피하기 위해서 수업에서는 코랩을 주로 사용합니다.

 

# OpenVINO 라이브러리 (간단한 객체 탐지) 사용 방식 

 

• 라이브러리 설정하기

# Install openvino package
%pip install -q "openvino>=2023.1.0"

import cv2
import matplotlib.pyplot as plt
import numpy as np
import openvino as ov
from pathlib import Path

# Fetch `notebook_utils` module
import urllib.request
urllib.request.urlretrieve(
    url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/main/notebooks/utils/notebook_utils.py',
    filename='notebook_utils.py'
)

from notebook_utils import download_file

 

먼저 위와 같은 라이브러리 환경을 준비해 주세요.

여기서 사용한 notebook_utils는 jupyter notebook 계열 사용자 편의성을 위해 OpenVINO에서 제공한 모듈입니다.

 

• 모델 가중치 준비하기

base_model_dir = Path("./model").expanduser()

model_name = "horizontal-text-detection-0001"
model_xml_name = f'{model_name}.xml'
model_bin_name = f'{model_name}.bin'

model_xml_path = base_model_dir / model_xml_name
model_bin_path = base_model_dir / model_bin_name

if not model_xml_path.exists():
    model_xml_url = "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2022.3/models_bin/1/horizontal-text-detection-0001/FP32/horizontal-text-detection-0001.xml"
    model_bin_url = "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2022.3/models_bin/1/horizontal-text-detection-0001/FP32/horizontal-text-detection-0001.bin"

    download_file(model_xml_url, model_xml_name, base_model_dir)
    download_file(model_bin_url, model_bin_name, base_model_dir)
else:
    print(f'{model_name} already downloaded to {base_model_dir}')

 

OpenVINO 프로젝트에서 몇몇 모델의 가중치를 공개했어요.

해당 코드를 사용하면 OpenVINO에서 공개한 가중치를 가져올 수 있습니다.

+) 만약 해당 모델 코드를 다운로드 받은 전적이 있다면 그냥 스킵합니다~

 

여기서는 'horizontal-text-detection-0001' 모델을 가져왔습니다!

이 모델은 수평 글씨를 감지하는 역할을 수행합니다.

 

• 추론 장치 선택

import ipywidgets as widgets

core = ov.Core()
device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value='AUTO',
    description='Device:',
    disabled=False,
)

 

위 코드를 동작시키면 GUI 형태의 '추론 장치 선택창'이 뜹니다.

여기서 장치를 지정해 주세요!

 

• 모델 로드하기

core = ov.Core()

model = core.read_model(model=model_xml_path)
compiled_model = core.compile_model(model=model, device_name="CPU")

input_layer_ir = compiled_model.input(0)
output_layer_ir = compiled_model.output("boxes")

 

'모델 가중치 준비하기' 파트에서 다운로드 받은 모델 가중치를 읽어들인 후, cpu 추론에 적합한 형태로 변형시킵니다.

다음으로 cpu 추론에 적합하게 변형된 컴파일 모델의 인풋, 아웃풋 형태를 지정해 줍니다.

여기서는 벡터 형태로 인풋을 받고, 아웃풋 형태로 박스를 출력한다고 코드로 나타냈습니다.

 

 

• 이미지 로드하기

# Download the image from the openvino_notebooks storage
image_filename = download_file(
    "https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/intel_rnb.jpg",
    directory="data"
)

# Text detection models expect an image in BGR format.
image = cv2.imread(str(image_filename))
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB));

# N,C,H,W = batch size, number of channels, height, width.
N, C, H, W = input_layer_ir.shape

# Resize the image to meet network expected input sizes.
resized_image = cv2.resize(image, (W, H))

# Reshape to the network input shape.
input_image = np.expand_dims(resized_image.transpose(2, 0, 1), 0)

 

 

해당 예제에서는 OpenVINO 라이브러리에서 예시로 제공한 이미지를 사용했어요.

+) 사용 이미지는 위와 같아요!

OpenVINO 라이브러리가 공개한 웹 이미지를 다운로드 받은 후, 이미지 정보를 각각의 변수에 저장합니다.

마지막으로 모델에서 요구하는 인풋 형태에 맞게 이미지를 조정합니다.

 

• 추론하기

%%time
# Create an inference request.
boxes = compiled_model([input_image])[output_layer_ir]

# Remove zero only boxes.
boxes = boxes[~np.all(boxes == 0, axis=1)]

# CPU times: user 314 ms, sys: 2.29 ms, total: 316 ms
# Wall time: 366 ms

 

윗 단계에서 로드한 이미지를 cpu 형태에 맞게 컴파일한 모델에 넣어 추론을 진행합니다.

%%time 주피터 노트북 전용 매직 커맨드를 사용하면 코드 동작 시 소요된 시간을 확인할 수 있습니다.

 

저는 총 316ms 걸렸습니다.

+) Wall time은 주피터 노트북 화면에 띄우는 데 걸리는 시간이므로, 첫 줄의 시간 결괏값만큼 중요하게 볼 필요는 없어요~

 

 

• 결과 시각화

# For each detection, the description is in the [x_min, y_min, x_max, y_max, conf] format:
# The image passed here is in BGR format with changed width and height. To display it in colors expected by matplotlib, use cvtColor function
def convert_result_to_image(bgr_image, resized_image, boxes, threshold=0.3, conf_labels=True):
    # Define colors for boxes and descriptions.
    colors = {"red": (255, 0, 0), "green": (0, 255, 0)}

    # Fetch the image shapes to calculate a ratio.
    (real_y, real_x), (resized_y, resized_x) = bgr_image.shape[:2], resized_image.shape[:2]
    ratio_x, ratio_y = real_x / resized_x, real_y / resized_y

    # Convert the base image from BGR to RGB format.
    rgb_image = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)

    # Iterate through non-zero boxes.
    for box in boxes:
        # Pick a confidence factor from the last place in an array.
        conf = box[-1]
        if conf > threshold:
            # Convert float to int and multiply corner position of each box by x and y ratio.
            # If the bounding box is found at the top of the image, 
            # position the upper box bar little lower to make it visible on the image. 
            (x_min, y_min, x_max, y_max) = [
                int(max(corner_position * ratio_y, 10)) if idx % 2 
                else int(corner_position * ratio_x)
                for idx, corner_position in enumerate(box[:-1])
            ]

            # Draw a box based on the position, parameters in rectangle function are: image, start_point, end_point, color, thickness.
            rgb_image = cv2.rectangle(rgb_image, (x_min, y_min), (x_max, y_max), colors["green"], 3)

            # Add text to the image based on position and confidence.
            # Parameters in text function are: image, text, bottom-left_corner_textfield, font, font_scale, color, thickness, line_type.
            if conf_labels:
                rgb_image = cv2.putText(
                    rgb_image,
                    f"{conf:.2f}",
                    (x_min, y_min - 10),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.8,
                    colors["red"],
                    1,
                    cv2.LINE_AA,
                )

    return rgb_image
    
    
if __name__ == "__main__":
    plt.figure(figsize=(10, 6))
    plt.axis("off")
    plt.imshow(convert_result_to_image(image, resized_image, boxes, conf_labels=False));

 

컴파일된 모델의 인풋 크기가 실제와 차이나기 때문에 아웃풋도 당연히 차이가 납니다.

따라서 결과값을 제대로 해석하고 싶으면, resize 전/후 비율을 알아야 해요.

해당 비율을 알아낸 후, box 결과값을 재해석하여 이미지 위에 출력하는 내용의 코드입니다.

여기서는 추가로 0.3 신뢰도를 넘는 박스만 그리도록 코드를 작성했네요.

 

 

코드를 동작시키면 위에 첨부한 이미지처럼 수평 텍스트를 박스 형태로 감지할 수 있습니다!

성능을 보니까 꽤나 쓸만 하지요? 

cpu 환경에서도 빠른 속도로 추론 가능한 모델이라서 더더욱 구미가 당겨요.

 

이런 이유들 때문에 저는 이번 주 수업에서 OpenVINO 라이브러리가 제일 흥미롭게 느껴졌어요.

아직 공식 document가 미흡한 부분이 있긴 하지만, 해당 라이브러리가 다루는 주제가 실용적이라서 제 눈길을 끌었습니다!

 

긍정적인 마음으로 OpenVINO 프로젝트를 더 지켜볼 생각입니다~

재미있는 오픈 프로젝트를 발견해서 기분이 좋아요 ^_^

 

● 개선

이번 주간에는 개인 공부로 밑시딥 1권의 '챕터6 : 학습 관련 기술들'에 대해 공부했습니다!

해당 챕터에서는 옵티마이저(optimizer), 가중치 초기화(weight initialization), 배치 정규화(batch normalization), 가중치 감쇠(weight decay), 드롭아웃(dropout) 등을 주로 다룹니다.

 

오늘은 여러 주제들 중에서 옵티마이저에 대해 더 깊게 다뤄보려고 합니다.

이전에는 여러 옵티마이저 종류에 따른 개념만 언급했었지요?

오늘은 코드도 함께 들여다 보려고 해요.

 

# 필요 라이브러리

import numpy as np
import numpy.typing as npt
import matplotlib.pyplot as plt
from typing import Dict, Callable

 

 

# SGD (Stochastic Gradient Descent)

class SGD:
    def __init__(self, lr:float=0.01):
        self.lr: float = lr
    
    def update(self, params: Dict, grads: Dict):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

 

모든 데이터를 대상으로 경사하강법을 수행하면 오랜 시간이 걸려요.

상당히 비효율적이지요.

 

이를 방지하기 위해 일부 데이터만 랜덤하게 뽑아서 연산하는 방식이 바로 'SGD'입니다!

가중치 업데이트 방식 자체는 GD(Gradient Descent) 방식과 동일하기 때문에, update 메서드 내용물도 똑같습니다.

+) 책에서는 일부 데이터를 뽑는 코드가 생략되어 있어요. 그건 np.random.radint 등을 활용하면 충분히 구현 가능합니다!

 

 

# Momentum

class Momentum:
    def __init__(self, lr:float=0.01, momentum:float=0.9):
        self.lr: float = lr
        self.momentum: float = momentum
        self.v: Dict = None

    def update(self, params: Dict, grads: Dict):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]

 

'Momentum'은 SGD에 이전 업데이트 방향까지 고려해서 관성을 부여한 방법입니다.

Momentum 기법은 관성을 추가로 적용함으로써 비교적 일정한 방향으로 가속할 수 있어요.

SGD에 비해 좀 더 일관성 있게 발걸음을 내딛는 느낌입니다!

 

 

# AdaGrad

class AdaGrad:
    def __init__(self, lr:float=0.01):
        self.lr: float = lr
        self.h: Dict = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

 

AdaGrad는 파라미터별로 학습률을 조정합니다.

이를 위해 h에 기존 기울기 값을 제곱하여 계속 더해준 값을 저장합니다.

 

이렇게 저장한 h를 사용하면 말이죠...

h에 저장된 값이 큰 파라미터는 적게 학습할 수 있고, 저장된 h 값이 작은 파라미터는 크게 학습하는 것이 가능해집니다!

 

 

# Adam

# The Adam optimizer motivated by 'Momentum' and 'RMSProp'. 
# So the Adam optimizer can accelerate the gradient descent algorithm and use an adaptive learning for each parameter.
class Adam:
    def __init__(lr:float=0.001, beta1:float=0.9, beta2:float=0.999):
        self.lr: float = lr
        self.beta1: float = beta1
        self.beta2: float = beta2
        self.iter: int = 0
        self.m: Dict = None
        self.v: Dict = None

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)

        self.iter += 1
        lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for key in params.keys():
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)

 

Adam은 Momentum과 RMSProp을 합친 optimization 기법입니다.

참고로 RMSProp은 AdaGrad를 더 발전시킨 형태라고 생각하시면 좋을 것 같아요.

대신에 RMSProp은 AdaGrad와 다르게 과거보다 최신 기록을 더 중요시하는 기법이라고 보시면 됩니다!

 

Adam은 두 기법을 합쳤기 때문에 파라미터별로 학습 정도를 조정할 수 있을 뿐만 아니라, 관성까지 부여해서 일관적 방향으로 학습하는 것도 가능합니다.

 

듣기만 해도 엄청 좋게 느껴지지요?

실제로도 여러 optimizer 사이에서 Adam optimizer 성능이 제일 높게 나오는 경우가 많아요.

단, 모든 문제의 해답이 Adam optimizer로 귀결되는 것은 아니므로 반드시 다른 optimizer도 함께 실험해 보는 것을 권합니다!

 

 

 

 

오늘 블로그 포스팅도 끝~

얼른 자고 일어나서 또 공부할 준비를 해야겠어요 :)

 

아자잣! 남은 50% 커리큘럼을 위해 힘내보겠습니다~!!

 

 

+) 부족한 부분이 있으면 댓글로 말씀해 주세요! 겸허한 마음으로 더 공부하겠습니다.