개념정리

  • 코드 포인트(code point): 각 유니코드 문자에는 고유한 코드포인트(숫자)가 할당되어있습니다. 이 유니코드 하나하나에 매핑되는 숫자를 코드포인트라고합니다. 1:1로 매핑되어있고, 코드포인트를 어떤 인코딩 방식을 하냐에 따라, 문자열이 달라질 수 있습니다.
  • 인코딩: 코드포인트를 바이트 시퀀스로 변환하는 규식을 일컫습니다.
  • 디코딩: 바이트 시퀀스를 코드포인트로 역변환 하는 규칙을 일컫습니다
  • 유니코드(Unicode): 모든 문자를 컴퓨터에서 일관되게 표현하고 다루기 위한 국제 표준입니다. 모든 문자열은 코드포인트를 이용하여, 1:1 매핑되어있습니다. 그리고 유니코드로 표현되는 경우 "U+"라는 접두사를 사용합니다.

 

코드포인트: 문자와 1:1로 매핑하는 숫자

코드포인트는 문자와 1:1로 매핑되는 숫자입니다. 예를 들어, `U+1f62e`와 같이 생긴 숫자가 코드포인트입니다. 유니코드의 코드포인트는 유니코드 표준으로 "U+"라는 접두사를 붙여서 4자리에서 6자리의 16진수로 표현합니다. `U+` 뒤에 1f62e은 각각이 16문자 문자열입니다. 이 문자는 😮의 이모지를 뜻합니다.

 

이 코드포인트를 UTF-8로 인코딩한다면, f0 9f 98 ae가 됩니다.

>>> code_point = int("1f62e", 16) # "U+1f62e"은 16진수로 표현합니다.
>>> print(code_point)
128558

>>> chr(code_point)
😮

>>> chr(code_point).encode("utf-8")
b'\xf0\x9f\x98\xae'

 

UTF-8은 가변 길이 인코딩 방식으로, 각 유니코드 코드 포인트를 1바이트에서 4바이트까지 다양한 길이로 인코딩합니다. 아래에 간단한 UTF-8 인코딩 테이블을 제시하겠습니다:

유니코드 범위 (16진수)UTF-8 바이트 시퀀스 (이진수)

U+0000 - U+007F 0xxxxxxx
U+0080 - U+07FF 110xxxxx 10xxxxxx
U+0800 - U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 - U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

여기서 "x"는 유니코드 코드 포인트의 비트를 나타냅니다. UTF-8에서 첫 번째 바이트는 0, 110, 1110, 또는 11110으로 시작하며, 다음 바이트는 항상 10으로 시작합니다. 이 테이블을 사용하여 유니코드 문자를 UTF-8로 인코딩할 수 있습니다.

이제 "b'\xf0\x9f\x98\xae'"을 디코딩 과정을 설명해 보겠습니다. UTF-8에서 각 문자의 첫 번째 바이트를 보고 이 바이트가 어떤 범위에 속하는지에 따라 그 문자가 몇 바이트인지 결정됩니다. 그런 다음 각 바이트에서 필요한 비트를 추출하여 이를 유니코드 코드 포인트로 변환합니다.

UTF-8 디코딩의 과정은 다음과 같습니다:

  1. 첫 번째 바이트를 확인합니다. => '\xf0'이며, 이는 이진수로 '1111 0000'을 나타냅니다. 이 바이트는 4바이트 문자의 첫 번째 바이트를 나타냅니다. 
  2. 첫 번째 바이트를 보고 문자의 길이를 결정합니다. => '11110000'은 네 번째 범위에 속하므로 이 문자는 총 4바이트로 구성됩니다. 
  3. 첫 번째 바이트의 마지막 4비트를 제외한 나머지 비트는 유니코드 코드 포인트를 나타냅니다. 여기서는 '1110'으로 시작하므로 이후 3바이트의 유니코드 코드 포인트가 함께 사용됩니다.
  4. 다음 바이트부터는 모두 '10'으로 시작해야 합니다. 여기서는 '\x9f', '\x98', '\xae'가 모두 이 조건을 충족합니다.
  5. 각 바이트에서 첫 번째 바이트 이후의 유니코드 코드 포인트를 나타내는 비트를 추출합니다. 이 비트는 모두 '10xxxxxx' 형식입니다. 따라서 각 바이트의 마지막 6비트를 추출하여 이를 합칩니다.
  6. 마지막으로 이진수를 십진수로 변환한 다음 이를 유니코드 코드 포인트로 해석합니다.
  7. 유니코드 코드 포인트를 유니코드 문자로 변환합니다.

 

또 다른 파이썬에서의 예시를 들어보겠습니다. 안녕하세요의 문자열이 있는 경우 `ord()`함수를 이용해서 10진법으로 표현할 수 있습니다. hex은 정수를 입력받아 16진법으로 표현합니다. 그렇기에, 이 10진법으로 표현한 것을 다시 16진법으로 표현하기위해서 `hex()`함수를 이용해서 변환합니다. 그리고 출력하면 아래와 같이 얻을 수 있습니다. 그리고 각 10진법으로 표현하면 숫자로만 열거됩니다. 

>>> text = "안녕하세요"
>>> code_points = [hex(ord(char)) for char in text]
>>> print(code_points)
['0xc548', '0xb155', '0xd558', '0xc138', '0xc694']

code_points = [ord(c) for c in text]
>>> print(code_points)
[50504, 45397, 54616, 49464, 50836]

 

`0x`은 파이썬에서 16진법을 표현하는 방식입니다.  "안"에 해당하는 문자는 10진법으로 50504이며, 이를 16진법으로 표현하면 "C548"입니다.

windows 계산기를 이용하여 50504을 16진법으로 변경한 예시

 

"안녕하세요"를 utf-8로 인코딩하면 아래와 같습니다. b로 시작하는 것은 바이트 리터럴을 나타냅니다. 파이썬에서 바이트 리터럴은 바이트 문자열을 나타내며, 이는 일련의 8비트 바이트로 구성된 바이트 시퀀스입니다.

 

인코딩을 하고하면, b''로 표현되는데 어떤문자는 문자 그대로 표현되고, 어떤 경우는 16진법으로 표현됩니다. 이 차이는 무엇일까요? 

아래의 예시를 하나 보겠습니다. apple은 utf-8의 경우는 b'apple이 출력됩니다. b''여도 16진수로 표현되는 경우가 있고, 그렇지 않은 경우가 있는데요. 아스키문자(공백부터 물결표(~))까지는 아스키 문자 그대로 출력이 가능한 경우는 그대로 출력됩니다. 그렇지 않은 경우는 16진수로 표현되기 때문입니다. 아스키코드 확인(URL)

>>> text ="안녕하세요"
>>> text.encode("utf-8")
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> text2 = 'apple'
>>> text2.encode('utf-8')
b'apple'

>>> text2.encode('utf-8')[0]
97

>>> text2.encode('ascii')[0]
97

 

아래의 아스키코드를 보면 Dec, Hx, Oct, Char가 있습니다. Dec은 Decimal(10진법)이며, Hx은 Heximal(16진법)입니다. Oct은 Octal으로 8진법입니다. apple을 utf-8로 인코딩한다음에 표현해보면, 97이나오고, 97에해당한는 문자(char)은 'a'입니다. 마찬가지로, 이를 ascii로 인코딩해도 97이나옵니다.

UTF-8와 ASCII는 대부분의 영문 알파벳 및 일부 특수 문자에 대해서는 동일한 방식으로 인코딩됩니다. ASCII는 UTF-8의 서브셋이며, UTF-8은 ASCII를 포함하고 있기 때문에 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩이 동일한 결과를 생성합니다.

따라서 'utf-8'과 'ascii' 인코딩을 사용하여 같은 문자열을 인코딩할 경우, 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩 결과가 동일할 수 있습니다. 이 경우에는 두 결과가 모두 97로 동일하게 나타나게 됩니다.

 

파이썬에서의 자료형: bytes, bytearray

  1. bytes:
    • bytes는 불변(immutable)한 바이트 시퀀스입니다. 즉, 한 번 생성되면 내용을 변경할 수 없습니다.
    • 바이트 객체는 0부터 255까지의 숫자로 이루어진 값들의 불변된 시퀀스입니다.
    • 주로 파일 입출력, 네트워크 통신 등에서 사용됩니다.
    • 예를 들어, b'hello'는 바이트 리터럴로, 문자열 'hello'를 바이트로 표현한 것입니다.
  2. bytearray:
    • bytearray는 변경 가능한(mutable)한 바이트 시퀀스입니다. 즉, 내용을 변경할 수 있습니다. 
    • 0부터 255까지 숫자로 이루어진 값들의 시퀀스입니다.
    • 바이트 배열은 bytes와 유사하지만, 변경 가능하고 가변성이 있습니다.
    • bytearray는 리스트와 유사하게 인덱싱 및 슬라이싱을 통해 요소를 수정하거나 추가할 수 있습니다.
    • 주로 이진 데이터를 변경하거나 조작할 때 사용됩니다.
    • 예를 들어, bytearray(b'hello')는 바이트 배열을 생성하고 초기값으로 문자열 'hello'를 사용한 것입니다.

간단히 말해, bytes는 불변하고 bytearray는 변경 가능한 바이트 시퀀스를 나타냅니다. 때에 따라서는 데이터를 수정해야 하는 경우 bytearray를 사용하고, 데이터를 변경할 필요가 없는 경우에는 bytes를 사용하는 것이 좋습니다.

In [23]: s = "안녕하세요"

In [24]: len(s)
Out[24]: 5

In [25]: s.encode('utf8')
Out[25]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

In [26]: encoded_s = s.encode('utf8')

In [27]: type(encoded_s)
Out[27]: bytes

In [28]: bytes("안녕하세요", encoding='utf-8')
Out[28]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

 

바이트(bytes)은 바이트어레이(bytearray)은 슬라이싱 및 인덱싱에 따라 아래와 같은 결과를 보입니다.

  • bytes의 슬라이싱: bytes
  • bytes의 인덱싱: int
  • bytearray의 슬라이싱: bytearray
  • bytearray의 인덱싱: int
>>> s = "안녕하세요"
>>> bytes(s, encoding='utf-8')
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> bytes(s, encoding='utf-8')[0]
236

>>> bytes(s, encoding='utf-8')[:1]
b'\xec'

>>> bytearray(s, encoding='utf-8')
bytearray(b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94')

>>> bytearray(s, encoding='utf-8')[:1]
bytearray(b'\xec')

>>> bytearray(s, encoding='utf-8')[0]
236

 

Struct module

파이썬 내장 모듈인 `struct` 모듈은 바이트로 전환하거나, 바이트를 문자열로 변경할 때, 주로 사용됩니다. 포맷을 지정하는 등의 역할도 가능합니다. `struct`모듈은 주로 아래와 같은 기능을 합니다.

  1. 패킹(Packing):
    • struct.pack(format, v1, v2, ...) 함수를 사용하여 파이썬 데이터를 이진 데이터로 변환합니다.
    • 이진 데이터는 바이트 문자열 또는 바이트 배열로 반환됩니다.
    • 주어진 형식(format)에 따라 데이터가 패킹됩니다. 예를 들어, I는 4바이트의 부호 없는 정수를 나타내는 형식입니다.
  2. 언패킹(Unpacking):
    • struct.unpack(format, buffer) 함수를 사용하여 이진 데이터를 파이썬 데이터 타입으로 변환합니다.
    • 주어진 형식에 따라 바이트 데이터가 해체됩니다.
    • 이 함수는 바이트 문자열 또는 바이트 배열을 받아서 주어진 형식에 따라 데이터를 언패킹합니다.
  3. 형식 지정자(Format Specifiers):
    • 패킹 및 언패킹 작업에 사용되는 형식 지정자는 바이트 순서, 데이터 유형 및 크기를 정의합니다.
    • 예를 들어, I는 부호 없는 4바이트 정수를 나타내며, f는 4바이트 부동 소수점을 나타냅니다.

 

Memory view

메모리뷰는 바이트 시퀀스를 생성/저장하는게 아니라(복사X), 공유메모리 방식으로 버퍼데이터를 조작할 때 주로 사용됩니다. 복사가 아니기에 직접 값을 변경은 불가능합니다.

메모리는 메모리에 대한 접근을 보다 효율적으로 다루는 데 사용됩니다. 아래는 memoryview의 간단한 예시입니다.

# 바이트 데이터로부터 memoryview 생성
>>> data = b'hello world'
>>> mv = memoryview(data)

# memoryview의 내용 확인
>>> print(mv)  # <memory at 0x7fbba30d2080>>

# memoryview를 이용하여 데이터 접근
>>> print(mv[0])  # 104 (b'h'의 ASCII 코드 값)
>>> print(mv[1:5])  # <memory at 0x7fbba98234c0>>
>>> print(bytes(mv[1:5]))  # b'ello' (슬라이싱된 바이트 데이터)

# memoryview를 이용하여 데이터 수정
>>> mv[6] = 66  # ASCII 코드 66은 'B' (원본 데이터가 수정불가, TypeError 발생)

 

예시: GIF포맷처리

GIF은 움짤로 주로 사용되는데요. GIF포맷을 struct을 이용해서 언패킹해보겠습니다.

GIF포맷은 아래와같이 포맷이 지정되어있습니다.

  • 0번부터 3바이트는 GIF을 의미하며,
  • 3번부터 3개의 바이트는 87a, 89a GIf 버전을 의미합니다. 
  • offset 6부터 2바이트: logicla width 
  • offset 8부터 2바이트: logical height

이를 파이썬으로 접근하면 아래와 같습니다. fmt의 "<"은 리틀엔디언, "3s"은 3개의 바이트, "H"은 하나의 정수를 의미합니다.

>>> import struct

>>>fmt = "<3s3sHH"

>>>with open("200w.gif", "rb") as fh:
   ...:     img = memoryview(fh.read())
   ...: 


>>> header = img[:10]
>>> header
<memory at 0x7f8b757bfac0>

>>> bytes(header)
b'GIF89a\xc8\x00\xff\x00'

>>> struct.unpack(fmt, header)
(b'GIF', b'89a', 200, 255)
  • <: 리틀 엔디안(Endian)을 나타냅니다. 리틀 엔디안은 낮은 주소부터 데이터를 읽음을 의미합니다. (파일 형식에 따라 다를 수 있으며, 여기서는 일반적으로 사용되는 리틀 엔디안을 가정합니다.)
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 형식을 나타내는 문자열입니다. "GIF"을 읽어왔습니다.
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 버전을 나타내는 문자열입니다. "89a"을 읽어왔습니다.
  • HH: 각각 2바이트(16비트)의 부호 없는(short) 정수를 나타냅니다. 여기서는 GIF 이미지의 가로 및 세로 크기를 나타내는데 사용됩니다.

src: https://www.researchgate.net/figure/GIF-Header-Format-adapted-from-14_tbl1_228678318

 

문제: tiff파일에서 바이트 조직

이 사이트에서 다운로드받은 tiff파일의 이미지 헤더를 다음을 참조하여(스펙), 이 파일 포맷에서 다음을 구하세요.

  • 바이트 오더: 리틀엔디언, 빅엔디언
  • tiff 파일 여부:
  • IFD 시작 오프셋:

 

 

 

이중 모드 스트링 / 바이트 API

바이트로 정규표현식을 만들면 "\d"와 같은 글자나 "\w"은 아스키문자만 매칭되지만 str로 이 패턴을 만들면 아스키문자외에 유니코드나 숫자도 매칭이된다. 

 

import re

# bytes로 정규표현식 패턴 컴파일하기
pattern_bytes = re.compile(rb'\d')

# bytes 대상으로 매칭 시도하고 모든 매칭 결과 찾기
binary_data = b'123 abc 456'
matches_bytes = pattern_bytes.findall(binary_data)

print("Matches with bytes regex pattern:", matches_bytes)
Matches with bytes regex pattern: [b'1', b'2', b'3', b'4', b'5', b'6']

 

import re

# str로 정규표현식 패턴 컴파일하기
pattern_str = re.compile(r'\d')

# str 대상으로 매칭 시도하고 모든 매칭 결과 찾기
text = '123 abc 456'
matches_str = pattern_str.findall(text)

print("Matches with str regex pattern:", matches_str)
Matches with str regex pattern: ['1', '2', '3', '4', '5', '6']
반응형

 

cuda toolkit 12.1 지원

pytorch 2버전이상부터는 cuda toolkit 12.1으로 사용하는게 pip 이나 설치가 편하기에, toolkit 12.1을 설치하려고함.

sudo ubuntu-drivers autoinstall

 

cuda toolkit 12.1 install

ubuntu 공식 도큐먼트(URL)을 따라서 설치하려고함. 

 

Trouble shooting 1: package has invalid Support PBheader, cannot determine support level

ubuntu 그레픽드라이버 유틸리티인 `ubuntu-drivers list`로 현재 설치가능한 드라이버를 찾다보면 위와 같은 경고를 볼 수 있는 습니다. 아래와 같이 해결했습니다 [URL]. 

$ sudo apt update
$ sudo apt upgrade

 

Trouble shooting 2: but it is not going to be installed

 

sudo apt remove nvidia*
sudo apt autoremove
반응형

 

DataParallel (DP)

DP은 데이터 병렬화 기술 중, 싱글노드에서만 사용할 수 있는 병렬화 기술입니다.

Forward and Backward passes with torch.nn.DataParallel. src: https://medium.com/huggingface/training-larger-batches-practical-tips-on-1-gpu-multi-gpu-distributed-setups-ec88c3e51255

위의 그림과 같이 포워딩(첫 행)에서는 배치를 GPU 개수만큼 쪼개 보내고, 모델도 복제(replicas)하고 각 장치에서 포워딩후에 그 결과값을 가져오는 형식입니다. 백워드에서는 [o1,o2,o3,o4]을 GPU-1에서 받은 것을 label과의 차이를 계산하여 각 손실 [loss1, loss2, loss3, loss4] 을 계산하여 각 gradient을 계산합니다. 이렇게 계산한 각 gradient은 다시 각자의 장치로보내 백워드를 하고, 다시 집계합니다.

def data_parallel(module, input, device_ids, output_device=None):
    if not device_ids:
        return module(input)

    if output_device is None:
        output_device = device_ids[0]

    replicas = nn.parallel.replicate(module, device_ids)
    inputs = nn.parallel.scatter(input, device_ids)
    replicas = replicas[:len(inputs)]
    outputs = nn.parallel.parallel_apply(replicas, inputs)
    return nn.parallel.gather(outputs, output_device)
  • `nn.parallel.replicate`: pytorch model을 각 장치에 복사합니다.
  • `nn.parallel.scatter`: pytorch 장치를 첫차원(=배치)에 대해서 복제합니다.
  • `nn.parallel.parallel_apply`: 복제된 모델에 입력값을 포워딩합니다.
  • `nn.parallel.gather`: output결과값을 `output_device`에 모아와 concat합니다.

 

아래와 같이 DP은 `torch.DataParallel(model)`의 원라인만 추가하면 사용할 수 있던 기술입니다 [1, 2]. 이 클레스는 데이터 병렬(DP)은 모듈레벨(torch.nn.module) 에서 손 쉽게 사용할 수 있도록 구현한 것입니다. 일단 데이터를 작은 배치사이즈로 나눈 후(split), 데이모델을 각 장치(GPU)에 복제한 후, 데이터를 fowarding합니다. 이후, 각 복제된 모델로부터 백워드(backpropagation)을 진행하고, 원본(original module, 첫 번째 장치에 있던)로 모듈로 가져와 집계합니다[3]. 

CLASS torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
  • module: pytorch model을 의미
  • device_ids:cuda devices (디폴트가 all devices)
  • output_devices: 복제한 모델로부터 집계할 장치를 의미합니다. (디폴트: device_ids=[0])

그렇기에, GPU수보다 배치사이즈가 커야합니다. 

import torch

model = Model(input_size, output_size)
if torch.cuda.device_count() > 1:
  print("Let's use", torch.cuda.device_count(), "GPUs!")
  # dim = 0 [30, xxx] -> [10, ...], [10, ...], [10, ...] on 3 GPUs
  model = nn.DataParallel(model)

model.to(device)

 

하지만, 이 데이터클레는 파이토치에서 권장하지 않는 클레스가 되었습니다. Single node multiple GPU인 경우에도 권장하지 않습니다. 대신. `DistributedDataParallel`을 권장합니다[4]. 이유는 명확히 나와있지 않지만,

  1. 데이터 핸들링의 요구사항이 정확히 충족되지않으면 병렬처리(multiprocessing)을 이용하는 CUDA model에서 경고가 있을 수 있거나, 부정확할 수 있어서 권장하지 않는 듯
  2. DataParalell은 Multithreading을 사용하기에 GIL이 걸리기 때문입니다.

그렇기에`DistributedDataParallel` 을 권장합니다. 이 클레스 내부적으로 멀티프로세싱을 GPU마다 하기때문입니다. 

또한, DataParallel을 사용하는 경우, 원본 모듈(pytorch model)의 key-value 형식으로 저장된 state_dict(=파라미터)의 key값이 달라지기 때문에, 주의를 요할 필요가 있습니다[5]. 위와 같이 `model = nn.DataParallel(model)`로 엮는 경우, 모델의 key값이 달라지기 때문입니다. 원래는 { 'conv1.weight': tensor(...), 'conv1.bias': tensor(...), 'fc.weight': tensor(...), 'fc.bias': tensor(...) }로 저장되던 { 'module.model1.conv1.weight': tensor(...), 'module.model1.conv1.bias': tensor(...),  로 저장되게됩니다.

그렇기에, 나중에 불러올 모델과 DP로 저장한 모델의 파라미터가 맞지않다는 github issue도 종종 볼 수 있습니다.

 

Distributed DataParallel (DDP)

DDP은 데이터 병렬화를 모듈레벨(모델레벨)에서 쉽게 구현한 것이고, 노드수(=머신수)가 여럿일 때도 사용할 수 있습니다. DPP은 병렬처리시에, multiprocessing으로 프로세스를 spawn(부모프로세스의 메모리를 복제하지 않는, 새롭게 프로세스를 실행시키는) 방식으로 진행합니다(spawn vs fork 설명)

  병렬처리방식 노드수 속도
DP Multithread (GIL발생) 싱글 노드 느림
DDP Multiprocessing 싱글 / 멀티 노드 빠름

 

파이토치 세팅은 내부통신이 여러방법이 가능한데, 별도의 설치는 필요없고 파이토치 패키지 내에 들어가있습니다(`torch.distributed`). 가장기본적으로 아래와 같이 할 수 있습니다.

싱글머신에서는 아래와 같이 진행합니다.

"""run.py:"""
#!/usr/bin/env python
import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp

def run(rank, size):
    """ Distributed function to be implemented later. """
    pass

def init_process(rank, size, fn, backend='gloo'):
    """ Initialize the distributed environment. """
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    os.environ['MASTER_PORT'] = '29500'
    dist.init_process_group(backend, rank=rank, world_size=size)
    fn(rank, size)


if __name__ == "__main__":
    size = 2
    processes = []
    mp.set_start_method("spawn")
    for rank in range(size):
        p = mp.Process(target=init_process, args=(rank, size, run))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

 

  • `init_process`: 마스터노드와 통신하기위해서 마스터노드의 호스트IP와, PORT을 지정합니다. backend은 어떻게 통신할 것인가에 대한 백엔드를 의미하는 것이며 `gloo`, `NCCL`, `MPI`, `Filesystem`, TCP`와 같은 백앤드가 가능합니다. 위 예시에서는 싱글 노드의 예시이므로 자기 자신 `127.0.0.1`로 통신하도록 되어있습니다.
  • `dist.init_process_group`: 프로세스 그룹을 초기화합니다. 인자 중 `rank`은 프로세스의 순위를 나타내며, 스폰되는 프로세스의 수만큼 0부터 N까지 랭크를 갖습니다. `world_size`은 스폰되는 전체 프로세스의 총 수를 의미합니다. 아래의 그림은 멀티프로세스 4개를 띄운 상태며, 0-3을 포함한 각각의 랭크를 지닌 프로세스입니다.

 

두 개의 프로세스를 띄워서 DDP로 병렬처리하는 코드는 아래와 같습니다.

import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn as nn
import torch.optim as optim
import os
from torch.nn.parallel import DistributedDataParallel as DDP


def example(rank, world_size):
    # create default process group
    dist.init_process_group("gloo", rank=rank, world_size=world_size)
    # create local model
    model = nn.Linear(10, 10).to(rank)
    # construct DDP model
    ddp_model = DDP(model, device_ids=[rank])
    # define loss function and optimizer
    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)

    # forward pass
    outputs = ddp_model(torch.randn(20, 10).to(rank))
    labels = torch.randn(20, 10).to(rank)
    # backward pass
    loss_fn(outputs, labels).backward()
    # update parameters
    optimizer.step()

def main():
    world_size = 2
    mp.spawn(example,
        args=(world_size,),
        nprocs=world_size,
        join=True)

if __name__=="__main__":
    # Environment variables which need to be
    # set when using c10d's default "env"
    # initialization mode.
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "29500"
    main()

 

Distributed Data-Parallel with multi-mode

멀티 노드로 분산처리하기위해서는 파이토치 유틸리치 중하나인 `torchrun`을 이용해야합니다.

  • torchrun을 이용하면 `run`, `world_size`을 명시적으로 전달할 필요도 없고 환경변수도 알아서 세팅해줍니다.
  • DDP을 이용할 때 사용하는 `torch.multiprocessing.spawn`도 사용할 필요없습니다.

 

각 머신에서 돌려야하는 소스코드는 아래와 같다고 생각해보겠습니다.

import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from datautils import MyTrainDataset

import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
import os


def ddp_setup():
    init_process_group(backend="nccl")
    torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))

class Trainer:
    def __init__(
        self,
        model: torch.nn.Module,
        train_data: DataLoader,
        optimizer: torch.optim.Optimizer,
        save_every: int,
        snapshot_path: str,
    ) -> None:
        self.local_rank = int(os.environ["LOCAL_RANK"])  # 머신 내 프로세스들의 랭크
        self.global_rank = int(os.environ["RANK"])       # 전체 머신에서의 각 프로세스의 랭크
        self.model = model.to(self.local_rank)
        self.train_data = train_data
        self.optimizer = optimizer
        self.save_every = save_every
        self.epochs_run = 0
        self.snapshot_path = snapshot_path
        if os.path.exists(snapshot_path):
            print("Loading snapshot")
            self._load_snapshot(snapshot_path)

        self.model = DDP(self.model, device_ids=[self.local_rank])  # DDP로 감싸줍니다.

    def _load_snapshot(self, snapshot_path):
        loc = f"cuda:{self.local_rank}"
        snapshot = torch.load(snapshot_path, map_location=loc)
        self.model.load_state_dict(snapshot["MODEL_STATE"])
        self.epochs_run = snapshot["EPOCHS_RUN"]
        print(f"Resuming training from snapshot at Epoch {self.epochs_run}")

    def _run_batch(self, source, targets):
        self.optimizer.zero_grad()
        output = self.model(source)
        loss = F.cross_entropy(output, targets)
        loss.backward()
        self.optimizer.step()

    def _run_epoch(self, epoch):
        b_sz = len(next(iter(self.train_data))[0])
        print(f"[GPU{self.global_rank}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")
        self.train_data.sampler.set_epoch(epoch)
        for source, targets in self.train_data:
            source = source.to(self.local_rank)
            targets = targets.to(self.local_rank)
            self._run_batch(source, targets)

    def _save_snapshot(self, epoch):
        snapshot = {
            "MODEL_STATE": self.model.module.state_dict(),
            "EPOCHS_RUN": epoch,
        }
        torch.save(snapshot, self.snapshot_path)
        print(f"Epoch {epoch} | Training snapshot saved at {self.snapshot_path}")

    def train(self, max_epochs: int):
        for epoch in range(self.epochs_run, max_epochs):
            self._run_epoch(epoch)
            if self.local_rank == 0 and epoch % self.save_every == 0:
                self._save_snapshot(epoch)


def load_train_objs():
    train_set = MyTrainDataset(2048)  # load your dataset
    model = torch.nn.Linear(20, 1)  # load your model
    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
    return train_set, model, optimizer


def prepare_dataloader(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=False,
        sampler=DistributedSampler(dataset)
    )


def main(save_every: int, total_epochs: int, batch_size: int, snapshot_path: str = "snapshot.pt"):
    ddp_setup()
    dataset, model, optimizer = load_train_objs()
    train_data = prepare_dataloader(dataset, batch_size)
    trainer = Trainer(model, train_data, optimizer, save_every, snapshot_path)
    trainer.train(total_epochs)
    destroy_process_group()


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description='simple distributed training job')
    parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
    parser.add_argument('save_every', type=int, help='How often to save a snapshot')
    parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
    args = parser.parse_args()
    
    main(args.save_every, args.total_epochs, args.batch_size)

 

multinode training을 할 경우에는 training job을 직접 SLURM이라는 스케쥴러를 이용해서 돌리거나, torchrun을 이용해서 각자 머신에 실행시키거나 해야합니다. 위의 코드는 `torchrun`으로 같은 rendezvous 인자를 전달해서 돌리는 예시입니다.

// node 1
$ torchrun \
--nproc_per_node=1 \
--nnodes=2 \
--node_rank=0 \
--rdzv_id=456 \
--rdzv_backend=c10d \
--rdzv_endpoint=127.0.0.1:29603 \
multinode_torchrun.py 50 10

// node 2
$ torchrun \
--nproc_per_node=1 \
--nnodes=2 \
--node_rank=1 \
--rdzv_id=456 \
--rdzv_backend=c10d \
--rdzv_endpoint=[node 1의 IP]:29603 \
multinode_torchrun.py 50 10

 

아래와 같이 올바르게 학슴됨을 확인할 수 있습니다.

 

 

Load map

노드 수, GPU, 스케쥴러 지원에 따라 데이터병렬화 방법

노드 수 GPU 수 데이터페러렐 방법 스케쥴러지원
Single-machine single GPU DistributedDataParallel NA
Single-machine multiple GPU DistributedDataParallel NA
Multi-machine multiple GPU torchrun   + rendezvous  X
Multi-machine multiple GPU SLURM 이용 SLURM

 

Trouble shooting 1): The IPv6 network addresses of (서버명, 55039) cannot be retrieved (gai error: -3 - Temporary failure in name resolution

- 도메인을 찾지 못하는 문제입니다. `DNS`쪽 문제일 수 있습니다. DNS 세팅을 다시하거나 서버명에 해당하는 내용을 `/etc/hosts`에 추가합니다.

저의 경우 아래와 같이, `etc/hosts`을 추가해서 해결했습니다.

heon@gpusvr04:~/repositories/misc$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 gpusvr04
***.***.***.*** gpusvr03

 

반응형

 

요약


Multi-task learning은 연관된 복수의 테스크를 하나의 모델에 학습시키면서, 추가적으로 성능이 올라갈 수 있어 종종 사용됩니다. 이 논문에서는 Multi-task learning시에 복수의 테스크들의 밸런스를 어떻게 주어야하는지, 불확실성(Uncertainity)을 기반으로 방법론을 제시합니다. 이 논문에서는 불확실성을 타나낼 수 있는 학습가능한 파라미터를 제시해서 손실함수에 함께 사용합니다.

 

Multi-task deep learning

아래의 이미지는 한 이미지로부터 서로 다른 3가지의 테스크을 수행하고, 3테스크의 손실함수를 합하여 최적화하는 일반적인 방법론입니다. 멀티테스크러닝(MTL)은 이런 유사한 테스크를 함께 사용하는 경우 하나의 테스크만 사용하는 것보다 더 높은 성능을 보일 수 있습니다.

 

문제는 이 테스크들의 각각의 손실함수 1), 2), 3)이 있을텐데, 이 테스크를 어떻게 가중치를 주어야하는지에 대한 고민입니다. 1:1:1이 최적일까요? 아닐 수 있습니다. 보통 이 가중치(비율)을 휴리스틱하게 여러 번의 실험을 하면서 실험적으로 구하기에, 매우 비용이 많이듭니다. 본 논문은 MTL시에 가중치를 불확실성기반으로 최적화하는 방법을 제안합니다.

 

Methods

MTL은 테스크가 N개면 N개의 손실함수를 보통 갖습니다. 이 논문에서는 2개로 가정합니다. 아래의 (7)에서 표기들은 각각 아래와 같습니다.

  • $\mathcal{L}(W,\sigma_{1},\sigma_{2})$: 최적화해야할 손실함수값입니다. $W,\sigma_{1},\sigma_{2} $각각의 인자에 따라서 값이 변화할 수 있음을 의미합니다.
  • $\mathbf{W}$: 모델의 trainable parameter 입니다
  • $\sigma_{1},\sigma_{2}$: 테스크 1, 테스크2에 대한 불확실성을 나타내는 trainable parameter입니다.
  • $f^{ \mathbf {W}}(x)$: 모델의 output입니다. 

(7)번식의 마지막 식을보면, 불확실성을 타나내는 파라미터가 각 테스크의 가중치로 있고, 마지막에 $log{\sigma_{1}\sigma_{2}}$에도 있습니다. 이는 불확실성($\sigma_{i}$)가 큰 경우 해당, 테스크의 손실함수를 별로 반영하지않기 위함입니다.

예를 들어보겠습니다. 1번 테스크의 불확실성이 큰 경우: $\sigma_{1}$이 커집니다. 따라서,$ \frac{1}{2\sigma_{1}^{2}}\mathcal{L}_{1}( \mathbf {W}) $은 작아집니다. 즉, 1의 불확실성이 큰 경우, 해당 테스크의 손실함수를 작게 반영합니다.

뒷항 $log{\sigma_{1}\sigma_{2}}$은 $ \sigma_{i}$가 무한히 커져 task1, task2의 손실함수($\mathcal{L}_{1}, \mathcal{L}_{2}$의 합이 무한이 작아짐을 방지하기위해서, log를 취하여 무한히 작아짐을 방지합니다.

 

Implementation

Pytorch Implementation이 있어, 이 깃헙의 주요 내용만 살펴보겠습니다.

  • 16번: $\sigma$을 trainiable parameter로 만들기위해서, 몇개의 sigma을 만들지를 인자로 받습니다.
  • 18번: 인자로 전달된 개수만큼 시그마를 1로 초기화합니다(=모든 테스크에 대해서 1로 초기화). gradient True로 최적화가 가능한 학습 가능한 변수로 합니다.
  • 23~23번: 각 손실함수값에 대해서 loss와 trainable parameters을 이용해서 위의 (7)식을 계산합니다.

 

 

Results

손실함수의 값을 어떻게 조정하냐에 따라서, 아래와 같은 Table1의 표를 보여줍니다. unweighted sum of losss에 비해서 모든 테스크의 지표들이 향상됨을 확인할 수 있습니다.

반응형

프롬프트 엔지니어링

프롬프트 엔지니어링은 거대한 언어 모델을 사용하여 특정 작업이나 질문에 대한 원하는 출력을 얻기 위해 입력 프롬프트를 조정하는 과정을 말합니다. 이는 모델의 출력을 조정하고 원하는 유형의 답변을 생성하기 위해 입력 텍스트의 형식, 콘텍스트(맥락), 질문의 구조 등을 조정하는 것입니다. 즉,  프롬프트 엔지니어링은 거대언어모델(LLM)에 원하는 출력을 얻기 위해 입력 프롬프트를 조작하는 기술입니다. 이 글을 Prompting guide의 내용을 좀 더 쉽게 풀어쓴 글입니다.

 

In-context learning (ICL)

인컨텍스트 러닝(In-context learning)은 모델이 주어진 프롬프트 내의 예시에서의 학습을 하는 것을 의미합니다. 즉, 파인튜닝(미세조정)과 다르게, "런타임(runtime)"에서 프롬프트로부터 학습해서 더 나을 결과를 내도록 하는 기술을 의미합니다. 보통의 머신러닝은 훈련데이터(training data)로부터 학습해서 모델이 고정되고, 더 나은 성능을 유도하기가 쉽지 않은데 반해서, ICL을 이용한 경우, 프롬프트엔지니어링으로 이 학습의 결과를 유도할 수 있습니다.

Following the paper of GPT-3 (Brown et al., 2020), we provide a definition of in-context learning: Incontext learning is a paradigm that allows language models to learn tasks given only a few examples in the form of demonstration.

 

프롬프트엔지니어링과 ICL의 차이를 구붆하자면, 프롬프트엔지니어링은 수단에 가깝고,더 큰 범위를 말하며, In-context learning은 학습의 목적에 가까운 것 같습니다. 어찌보면, Few-shot과 유사합니다. Few-shot learning과 딱히 구별하기 어렵기도 합니다. 그래서 ICL을 few-shot learning, few-shot prompting이라고하기도합니다 [REF].

In-context learning (ICL) is known as  few-shot learning  or  few-shot prompting

 

Zero-shot

제로샷은 학습해본 적없는 클레스에 대해서, 분류(Classification)하는 것을 의미합니다. 실제로 언어모델은 감정분류에 대해서 학습한적은 없습니다. 

// 프롬프트
중립, 긍정, 부정으로 감정을 분류하세요.
오늘 코스피는 전일 대비 -2% 마감이었습니다.
감정: 

// 결과
부정

 

Few-Shot prompting

위에서 보듯, Zero-shot도 어느정도 되지만, 복잡한 문제들은 실패합니다. 대신에 Few-shot은 Zero-shot과 대비하여, 몇 가지 추가적인 예시(질문-답변)들을 좀 더주는 것입니다. 컨텍스트 상에서의 학습을 유도하는 것입니다. reversed label을 주어도 잘 학습합니다.

//프롬프트
아래의 예시문장들을 참고하여, <문제>에 해당하는 다음의 문장을  긍정, 부정으로 감정을 분류하세요

<예시>
질문1: 오늘 코스닥은 전일 대비 +5% 상승 마감하였습니다.
정답1: 부정
질문2: 오늘 코스피는 전일 대비 -2% 마감이었습니다.
정답2: 긍정
질문3: 오늘 나스닥은 중동 전쟁으로 인해 전일 대비 -2% 마감이었습니다.
정답3: 긍정
질문4: 코스피, 코스닥은 전일 대비 +5% 상승 마감하였습니다.
정답4: 부정
질문5: 미국 나스닥은 전일 대비 +5% 상승 마감하였습니다.
정답5: 부정
</예시>

<문제>
질문: 오늘 코스피는, 전일 대비 +5% 상승 마감하였습니다.
정답: 

// 결과
부정

 

Chain-of-Thought (CoT)

CoT기법의 핵심적인 내용은 사고과정의 중간내용을 추가(생성, 제공)하는 것입니다. 논리적인 사고의 중간내용을 추가로 작성해주어서, 복합한 문제에 대한 더 나은 성능을 기대할 수 있습니다. CoT가 가능한 이유는 GPT는 자기회기(Auto regressive)한 모델이기에, 여태 나온 문장의 결과를, 다시 입력값으로 사용하게됩니다. 즉, 현재의 반환값이, 미래의 입력값이 되기에 중간중간 어떤 텍스트를 생성하는지가 중요합니다. (https://arxiv.org/abs/2201.11903)

아래의 우측그림과 같이 정답이 유도되는 과정을 (A)에 추가로 기입해주기에, 자기회귀 중간에서 잘못된 답을 내놓지 않도록 유도하고, 결과적으로 더 나은 답을 유도하게 할 수 있습니다.

Figure 1: Chain-of-thought prompting enables large language models to tackle complex arithmetic, commonsense, and symbolic reasoning tasks. Chain-of-thought reasoning processes are highlighted. src:&nbsp;https://arxiv.org/pdf/2201.11903.pdf

일반적인 프롬프트에서 작성하면 아래와 같이 틀린답을 얻을 수 있습니다.

문제: 저는 사과가 10개 있었습니다. 2개를 이웃에게 나눠주고, 2개는 사과가 상해서 버렸습니다. 그리고 5개의 사과를 더 샀고, 1개는 먹었습니다. 몇개가 남았나요?

정답: 12개가 남았습니다.

 

단계별로 사고하라는 말을 추가로 넣어보았습니다.

문제: 저는 사과가 10개 있었습니다. 2개를 이웃에게 나눠주고, 2개는 사과가 상해서 버렸습니다. 
그리고 5개의 사과를 더 샀고, 1개는 먹었습니다. 몇개가 남았나요?  
단계별로 생각해서, 단계별 사과의 수를 알려주고, 최종적으로 남은 사과의 수를 알려주세요. << 추가

정답: 

1단계: 사과 10개
2단계: 이웃에게 2개를 나눠줌 -> (10 - 2 =) 8개의 사과가 남음
3단계: 2개를 버림 -> (8 - 2 =) 6개의 사과가 남음
4단계: 5개를 추가로 삼 -> (6 + 5 =) 11개의 사과가 있음
5단계: 1개를 먹음 -> (11 - 1 =) 10개의 사과가 남음

최종 정답: 남은 사과는 10개입니다.

 

 

SC(Self-Consistency)

프롬프트 엔지니어링 중에 가장 성능이 좋은 기법입니다 (https://arxiv.org/abs/2203.11171). CoT을 한 프롬프트를 N번 포워딩하여, 집계하는 방식으로 앙상블에 가까운 방식입니다. 아래의 3가지 순서로 이뤄집니다.

  1. CoT을 이용해서 프롬프팅을 합니다. 아래의 Figure 1과 같이 CoT을 이용해서, 사고과정에서의 중간과정을 해야한다는 컨텍스트를 제시하면서, A을 남겨 completion문제로 만듭니다.
  2. CoT의 과정과 유사하게 여러 사고방의 중간을 넣어줍니다. 
  3. N개의 답변을 획득한 후 하나의 답변으로 요약합니다.

주의할 것은 여러 답변을 얻어내야하기 때문에, Temperature(randomness의 정도)을 0으로 하면 안됩니다.  또한, 언어모델을 여러번을 추론해야하기 때문에, 그 만큼의 연산비용도 같이 요구됩니다.

 

RAG(Retrieval Augmented Generation)

언어모델에서 가장 큰 문제는 환각(Hallucination)입니다. GPT같은 생성형 언어모델 특성상 모른다고 말하는 것이 아니라, 다음 단어(Next token)을 잘 예측하는 것이 주류기이 때문에 그렇습니다. 이 환각 현상을 줄이기 위해서, 내용의 맥락(Context)을 함께 사용되는 방법이 널리 사용됩니다. 이 방법이 RAG(Retrieval Augmented Generation)입니다. 즉, 어떤 정보를 검색(Retrieval)하여 얻은 내용을 포함(증강, Augmented)하여 언어를 생성(Generation)합니다.

이 RAG은 특수 목적(Task-specific)한 내용을 생성할 때 더 유리하게 사용됩니다. 일반적인 언어모델은 복잡한 내용을 이해하기보다는 일반적인 다음단어를 생성하기 위함이기 때문입니다.

  1. 입력으로 프롬프트를 던지고, 이 프롬프트와 가장 유사한 도큐먼트를 찾습니다.(위키피디아일수도 있고, vector DB로 만들어놓은 문서-벡터 DB일수도있습니다). 이 도큐먼트를 context로 사용됩니다.
  2. 원본 프롬프트와 도큐먼트(context)을 합쳐(concat)하여 final output을 만듭니다.

Image Source:&nbsp; Lewis et el. (2021)

ReAct (Synerzing reasoning and Acting in Language models)

ReAct은 "사고과정(Reasoning trace, 추론) -> 행동(task-specific actions)"을 반복해서 돌리는 방법입니다. CoT와의 가장 구별되는 차이점은 실제 "행동(action)"이 들어간다는 것입니다.

 

아래의 그림은 "Thought, Act, Obs"의 3가지 단계를 구분지어서, 각 상황을 설명해보라고 하는 것입니다. 이 3가지 단계를 하나의 회차(에폭, 또는 Set)으로 두어, 다음 Set의 예측에 활용하는 것을 의미합니다.

실제 엑션이 가능해야하니, 액션을 할 수 있는 자료형을 얻거나/검색하는 기능이 구현이 되어야합니다.

Set 1

  • 사고1: 첫회차에는 애플리모트(장비)가 필요한데, 원래 애플장치랑 상호작용할 수 있는 프로그램을 찾아달라고합니다.
  • 액션1: 실제 애플리모트를 검색합니다.
  • 관찰1: 애플리모트에 대한 설명이 나옵니다. Front Row media center program에 대한 내용도 반환합니다.

Set 2

  • 사고2: 관찰1에서 얻은 내용을 다시 입력합니다.
  • 액션2: 사고2의 내용을 바탕으로 액션을 다시 시행합니다.
  • 관찰2: 액션의 2의 결과를 요약합니다.

 

아래의 예시에서는 사고-액션-관찰의 3가지 구성으로 사이클을 돌리는 방법입니다. 사고1에서는 애플리모컨으로 조절할 수 있는 장치를 찾고있고, 액션1에서는 실제 검색을 진행을하고, 관찰1에서는 검색의 결과를 가져옵니다. 이후, 사고2은 관찰1의 결과를 정리하고, 액션2은 사고2의 내용을 검색하고, 관찰2은 액션2의 결과를 다시 표현합니다. 이 사이클을 정해진 수만큼 반복합니다.

ReAct 예시, src: Yao et al., 2022

 

아래의 그림은 ReAct의 차이점을 Reason Only, Act only와 강조하기 위해 구분지어놓은 그림입니다.

  • Reason Only은 사고과정을 추가하면서 더 나은 결과를 얻는 방법입니다. 예로는 CoT가 있습니다.
  • Act Only은 RAG과 같이 환경(Environment)에서 Actions(검색)의 결과를 다시 출력에 활용하는 방법입니다.
  • ReACT은 사고과정과 액션을 둘 다 하는 방법입니다.

 

ReAct, CoT(Reason ony), Act Only(e.g. RAG)차이

References

https://www.promptingguide.ai

반응형

 

Data-Distortion Guided Self-Distillation for Deep Neural Networks , AAAI 2019

 

배경: Knowledge distillation 

큰 모델의 model confidence (softmax output)은 이 큰 모델이 학습한 지식을 담고있지 않을까라는 가설로 시작됩니다. 큰 모델의 결과물(증류된 정보)을 이용해서 작은 모델을 간단히 학습할 수 있지 않을까라는 생각입니다 [1]. 즉 큰 모델의지식(knowledge)이 model confidences 분포이겠구나라는 것입니다. 

아래의 그림과 같이 Teacher model이 훨씬 큰 딥러닝모델이고, Student모델이 더 작은 모델입니다. 큰 모델의 예측결과(model confidences)자체가 큰 모델의 지식이라는 것입니다. 예를 들어, 고양이가 6번인덱스의 클레스였다면, 0번은 낮고, 1번도 낮고..., 6번이 제일 큰데 0.4정도라는 이 분포자체가 Teacher모델이 갖고있는 큰 파라미터의 지식이 됩니다. 이를 답습하자는것이 Student모델의 학습목표입니다.

Knowledge distillation 방법론

흔히 지식증류(knowledge distillation)방법들은 학습방법에 따라, 3가지로 나뉩니다. 

  1. teacher-to-student: 큰 모델의 지식(logits, feature map,attention map등)을 작은 모델이 더 잘학습시키기 위한 방법론. 서로 다른 모델이 2개가 있어야하며, Teacher model은 학습되어있다고 가정합니다.
  2. student-to-student: 서로 다른 작은 다른 모델(student)을 서로 다른 초기값을 가지게하고, 학습을 동시에 해가면서 지식의 차이를 점점 줄여가나는 방식입니다. teacher-to-student와는 다르게 이미 사전학습된 모델이 필요없는 On-line training 방법입니다.
  3. self-distillation (self-teaching): 하나의 네트워크를 이용하며, 이미지 1개를 데이터 증강(random, rotation)등을 통해서 2개로 만들고, 서로 다른 지식을 일치시키는 방법을 사용합니다. 즉, 하나의 네트워크를 사용하며, 서로 다른이미지를 추론했음에도 분포가 같아야하는 모델 일반화가 더 잘되는 방법으로 학습을 유도합니다.

 

방법: Self-distillation

Self-distillation은 크게 4단계로 이어집니다.

1. Data distortion operation: data augmentation 방법과도 같습니다. 같은 원본데이터로, 서로 다른 두 이미지를 생성합니다.

2. Feature extraction layers: 하나의 모델에 두 증강된 이미지를 통과시켜 특징값(feature)을 얻습니다. 여기서는 두 특징값(벡터)의 차이를 계산합니다. 

MMD은 Maximum Mean Discrepancy을 의미하며, 두 증강된 이미지를 특징으로 만든 벡터의 차이를 의미합니다. 수식으로는 아래와 같습니다.

$MMD_{emp}=||\frac{1}{n}\sum_{i=1}^{n}\mathbf{h}(x_{ai})- \frac{1}{n}\sum_{i=1}^{n}\mathbf{h}(x_{bi})||_{2}^{2}$

$ \mathbf{h} $은 네트워크를 의미합니다. 즉 $ \mathbf{h} (x_{ai}), \mathbf{h} (x_{bi})$ 은 서로 다른 이미지를 하나의 네트워크에 태워 얻은 벡터를 의미합니다. 이 벡터의 차이를 계산하는 것이 MMD입니다. 

3. Classifier: classifier레이어에 통과시킵니다.

4. Predictor: 사후확률($\mathbf{p}(y|\mathbf{x}_{i})$)을 이용해서 2가지를 계산합니다. 하나는 분포의차이(KL divergence), 또 하나는 classification loss (cross-entropy loss)입니다.

  • KL divergence은 방향이 없기에 양방향으로 $KL(\mathbf{p}_{a}, \mathbf{p}_{b}), KL(\mathbf{p}_{b}, \mathbf{p}_{a})$을 둘 다 계산합니다. 
  • Classification loss은 두 이미지에 대해서 라벨을 이용해서 classification을 계산합니다.

KL diveregnce loss 2개, classification loss2개, MMD loss을 합산, 총 5개의 손실함수를 합산하여 모델을 학습시킵니다.

Results

첫 번째 결과는 CIFAR-100에서의 분류성능이고 동일한 파라미터수에서는 가장 낮은 테스트에러를 보였습니다.

두 번째, 결과는 모델의 크기에따라, self-distillation을 할때 어느정도 큰 차이가 나는지를 확인해봤습니다. 작은모델에서 distillation할때 성능이 크게 향상되는 것을 볼 수 있습니다. 즉, 이 방법론을 사용할거면 작은모델을 사용하는 접근이 유리해보입니다.

세 번째, 결과는 다양한 네트워크를 이용한 종합적인 성능인데, 모두 SD(Self-distilation)을 이용하는 경우 더 좋은 성능을 보였습니다.

네 번째 결과로, 데이터가 매우 큰 경우에서도 잘 동작하는지를 봤는데, 데이터가 매우 큰 경우에는 성능향상이 있었는데, 그렇게 큰 차이는 아닌 것 같내요.

Self-supervised learning vs self-distillation

두 방법론은 1)augmetentation이 둘 다 사용되고, 2) 하나의 네트워크를 학습한다는 것에 공통점이 있습니다.

하지만, SSL(Self-supervised learning)은 라벨이 필요없는 데이터로 학습한다는 것에 큰 차이가 있습니다.

SD(Self-distillation)은 총 5가지의 손실함수를 사용하는것중에 2개의 손실함수는 classification loss가 사용되니 라벨이 반드시 필요한 방법론이 됩니다.

 

References

[1] https://arxiv.org/abs/1503.02531

반응형

API 요청을 관리하는 데에는 여러 이유가 있습니다. 그 중에서도 토큰 버킷을 사용하는 이유는 다음과 같습니다:

  1. 트래픽 제어: 토큰 버킷을 사용하면 API로 들어오는 트래픽을 제어할 수 있습니다. 이를 통해 서버에 과도한 부하가 걸리는 것을 방지하고, 서비스의 안정성을 유지할 수 있습니다.
  2. 사용량 제한: 토큰 버킷을 이용하면 API를 사용하는 클라이언트의 요청 수를 제한할 수 있습니다. 이는 과도한 사용량으로 인한 서버 과부하를 방지하고, 공정한 서비스 이용을 보장합니다.

 

TokenBucket 만들기

토큰 버킷 패턴은 고정된 속도로 토큰을 생성하고, 요청이 들어올 때마다 토큰을 소비하여 일정량의 토큰을 가지고 있는지 확인하여 처리하는 방식입니다. 알고리즘은 아래와 같습니다.

  1. TokenBucket은 버켓(바구니)가 있다고 가정합니다.
  2. 요청이 들어오면, 버켓에서 1개를 소비해서 그 다음의 로직을 진행시킵니다. 요청이 들어왔을 때 버킷에 토큰이 없으면 drop합니다.
  3. 고정된 속도(r)로 버킷에 토큰채웁니다. 

https://www.researchgate.net/figure/Diagram-representing-token-bucket-algorithm_fig1_351452059

 

TokenBucket 을 middle ware 에 추가

- FastAPI의 starlette을 베이스로 작성되어있어서, 이 미들웨어를 서브클레싱해서 손쉽게 token bucket을 사용할 수 있습니다.

- starlette.middleware.base.BaseHTTPMiddleware을 초기화시 app을 받게되어있고 이 베이스클레스는 dispatch라는 함수를 구현하게어 있습니다.

class BaseHTTPMiddleware:
    ...
    
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        raise NotImplementedError()  # pragma: no cover

 

dispatch을 함수를 아래와 같이 구현해서 bucket.consume_token()이라는 함수가 bool으로 false or True을 구현하게 함에따라, 예외를 일으킬것인지 다음 로직을 실행시킬것인지를 결정하게 작성합니다.

 

위의 구현을 위해 아래와 같이 tokenbucket을 정의합니다.

초기화시, 버킷의 토큰 최대 저장량(max_len)과 토큰을 채우는 시간(r, refill_rate)을 지정하여 아래와 같이 작성합니다.

 

실행

- 다음과 같이 python3으로 fastAPI을 구동시킵니다.

(misc) (base) heon@gpusvr03:~/repositories/misc/tokenbucket_ex$ python3 app.py 
INFO:     Started server process [4121818]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:30000 (Press CTRL+C to quit)

 

그 후 요청 15개를 보내봅니다. 아래와 같이 10개를 토큰을 다 소비한경우 5개의 요청이 예외처리됩니다.

>>> import requests
>>> requests.get("http://0.0.0.0:30000/")
<Response [200]>
>>> for _ in range(15):
...     requests.get("http://0.0.0.0:30000/")
... 
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [500]> # 토큰 10개 소비되어 예외처리
<Response [500]>
<Response [500]>
<Response [500]>
<Response [500]>

 

반응형

"-e" 옵션은 설치한 경우 파이썬 패키지를 담는 "site-packages" 디렉토리에 설치되는 것은 동일하나, 심볼릭 링크로 설치가되어 원본소스코드가 변경된 후, 별도의 pip install 명령어 없이, import시 바로 변경사항이 반영되는 옵션

 

pip install -e 옵션

종종 github에서 `pip install -e .`로 "-e"옵션을 함께 전달하여 파이썬 패키지를 설치하라는 리포지토리가 있습니다

 

예시로 아래와 같은 패키지 디자인의 경로가 있다고 가정합니다. 

(camelyon) @gpusvr03:~/repositories/camelyon$ tree -L 1
.
├── camelyon
├── data
├── LICENSE
├── logs
├── notebooks
├── README.md
├── setup.py
├── tiling_version16.py
├── tiling_version17.py
└── utils.py

이 패키지의 setup.py에는 다음과 같이 설치 명령어가 있습니다. 이 setup.py을 이용하여 "$pip install ."을 해보겠습니다.

아래와 같이 잘 설치됨을 알 수 있습니다. 패키지 설치장소는 가상환경인 "/home/heon/anaconda3/envs/camelyon"에 설치되었습니다. 

 

 

설치한 패키지를 들어가보면, setup.py에서 지정한 p.y파일들이 들어가있습니다.

 

이 패키지를 지우고, pip install -e 옵션으로 설치해보겠습니다. 패키지가 설치된 장소를 들어가보면, *.egg-link만 있습니다.

이 링크의 내용물은 경로만 담겨져 있습니다.

 

*egg.link 파일은 내부에 파일하나나 디렉토리만 담고있습니다. 심볼릭링크를 지원하기위한 파일입니다. 즉, 설치는 했지만 바라보는 소스코드는 원본을 바라보고 있어서, 디렉토리의 변경사항을 바로 추적할 수 있습니다.

https://wiki.python.org/moin/PythonPackagingTerminology

반응형

 

 

 

회전변환시에 필요한 matrix을 roration matrix라고하며, 2D와 같이 표현할 수 있습니다.

$M(\theta)=
\begin{bmatrix}
    cos\theta & -sin\theta \\
    -sin\theta & cos\theta
\end{bmatrix}$

이 공식의 유도과정을 이해해보겠습니다.

그림1.

위 그림과 같이 구하고자하는 평면에 두 벡터가 있습니다. 이 그림의 요소들은 다음과 같습니다.

  • G(x, y): 회전시키기 전 벡터
  • G'(x',y'): G을 $\theta$만큼 회전시킨 벡터, 
  • r:G벡터와 G'벡터 길이
  • $\theta$: G을 G'으로 반시계방향(counter-clockwise)으로 회전한 벡터

위 그림에 따라 x, y은 아래와 같이 표현할 수 있습니다.

  • $ x=r cos v $  
  • $ y=r sin v $
  • $ x'=rcos(v+\theta)$
  • $ y'=rsin(v+\theta)$

3번 째, 4번째 식의 $ v+\theta$은 삼각함수의 덧셈정리로 풀면 아래와 같습니다.

  • $x'=rcos(v+\theta) = r(cos(v)\cdot cos (\theta) -sin(v)\cdot sin (\theta))$  (코코마사사)
  • $y'=rsin(v+\theta) = r(sin(v) \cdot cos (\theta) + cos(v) \cdot sin(\theta))$ (싸코코)

위 식에서 $cosv$ 와 $sinv$은 이미 알려져 있으니, 대입하여 알 수 있습니다. 

  • $x'=r(x \cdot cos(\theta) - y \cdot sin(\theta))$
  • $y'=r(x \cdot sin(\theta) + y \cdot cos(\theta))$  (x을 앞으로 정렬)

위 식에서 r이 1인 경우는 단순히 선형결합형태이므로, 선형결합 형태로 나타낼 수 있는 메트릭스로 표현 할 수 있습니다.

$\begin{bmatrix} x' \\ y' \end{bmatrix}= 
\begin{bmatrix}
    cos\theta & -sin\theta \\
    sin\theta & cos\theta
\end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}$

 

Python implementation


파이썬으로 위의 방법을 구하면 아래와 같이 작성할 수 있습니다. 주의할 것은 여기서 는 각도이며, 여기서는 30도를 라디안 단위로 변환하여 사용해야 합니다.

import numpy as np

# 각도를 라디안으로 변환
theta = np.deg2rad(30)

# 회전 행렬 생성
R = np.array([[np.cos(theta), -np.sin(theta)],
              [np.sin(theta), np.cos(theta)]])

# G 벡터 생성 (임의의 값으로 설정)
G = np.array([1, 0])  # 예를 들어, G = (1, 0)으로 설정

# G' 벡터 계산
G_prime = np.dot(R, G)

print("G' 벡터:", G_prime)
print("회전 행렬:")
print(R)

 

References


https://www.cuemath.com/algebra/rotation-matrix/

반응형

요약


  • 메인루틴(Main routine): 메인루틴은 보통 프로그램의 시작점이며, 프로그램의 주 흐름을 담당합니다. 메인루틴은 일련의 작업을 수행하고 다른 서브루틴이나 코루틴을 호출할 수 있습니다. 프로그램이 시작되는 메인코드라고 생각하면 됩니다.
  • 서브루틴(subroutine): 메인루틴에서 호출되는 함수, 또는 서브루틴에서 호출되는 함수들을 의미합니다. 즉, 다른 루틴에서 호출되는 경우를 의미합니다.
  • 코루틴(Coroutine): 메인루틴에서 호출되지만, 코루틴은 실행되는 도중에, 일시중단되어 다시 메인루틴으로 전환되었다가 다시 코루틴으러 전환될 수 있는 "제어흐름"이 가능한 루틴을 의미합니다.

 

코루틴, 메인루틴, 서브루틴은 프로그래밍에서 중요한 개념이며, 이를 이해하는 것은 프로그래밍의 성능을 향상시키는 데 도움이 됩니다. 각각의 이유를 자세히 설명하겠습니다.

  1. 프로그램의 구조 이해(메인루틴/서브루틴의 이해):
    • 메인루틴은 프로그램의 주요 흐름을 담당하며, 프로그램의 시작점입니다. 이를 이해함으로써 프로그램이 어떻게 실행되는지 전반적으로 이해할 수 있습니다.
    • 서브루틴은 재사용 가능한 작은 조각의 코드를 나타냅니다. 이를 통해 코드를 더 모듈화하고 유지 보수하기 쉽게 만들 수 있습니다.
  2. 코드의 재사용과 모듈화(서브루틴/코루틴의 이해):
    • 서브루틴을 사용하면 비슷한 작업을 수행하는 코드를 여러 곳에서 재사용할 수 있습니다. 이로써 코드의 중복을 피하고 유지보수성을 향상시킬 수 있습니다.
    • 코루틴은 서브루틴과 유사하지만, 일시 중단되고 다시 시작될 수 있기 때문에 작업의 흐름을 제어하는 데 유용합니다. 이를 통해 비동기 작업이나 상태 유지가 필요한 작업을 효율적으로 처리할 수 있습니다.
  3. 동시성과 비동기 프로그래밍(코루틴의 의해):
    • 코루틴은 동시성과 비동기 프로그래밍에서 중요한 역할을 합니다. 코루틴을 이용하면 여러 작업을 동시에 실행하고 작업 간에 상태를 공유할 수 있습니다. 이를 통해 복잡한 동작을 효과적으로 조정하고 성능을 향상시킬 수 있습니다.
  4. 효율적인 코드 실행:
    • 코루틴은 작업의 일시 중단과 다시 시작을 가능하게 함으로써 I/O 바운드 작업을 효율적으로 처리할 수 있습니다. 이를 통해 대기 시간이 긴 작업을 실행하는 동안 다른 작업을 수행할 수 있으며, 시스템 자원을 더 효율적으로 활용할 수 있습니다.

이러한 이유들로 인해 코루틴, 메인루틴, 서브루틴을 이해하는 것은 프로그래밍에서 중요한 역할을 합니다. 이러한 개념을 잘 숙지하고 활용함으로써 보다 효율적이고 유연한 코드를 작성할 수 있습니다.

 

메인루틴(Main routine)


메인루틴은 프로그램의 주요 흐름을 담당하는 부분으로, 일반적으로 프로그램의 시작점이며 종료 지점이 될 수도 있습니다. 메인루틴은 전체 프로그램의 실행 흐름을 정의하고 조절하며, 다른 서브루틴이나 함수를 호출하여 작업을 수행하고 그 결과를 처리합니다.

메인루틴은 보통 다음과 같은 역할을 수행합니다:

  1. 프로그램의 시작점: 메인루틴은 일반적으로 프로그램이 실행되면 가장 먼저 실행되는 부분입니다. 프로그램의 시작 지점을 정의하고 초기화 작업을 수행합니다.
  2. 주요 작업 수행: 메인루틴은 프로그램이 수행해야 하는 주요 작업을 정의하고 호출합니다. 이는 사용자의 입력을 받는, 데이터를 처리하는, 다른 모듈이나 서브루틴을 호출하는 등의 작업을 포함할 수 있습니다.
  3. 서브루틴 호출: 메인루틴은 필요에 따라 다른 서브루틴이나 함수를 호출하여 작업을 분할하고 모듈화합니다. 이를 통해 코드를 재사용하고 유지보수성을 향상시킬 수 있습니다.
  4. 프로그램의 종료: 메인루틴은 프로그램이 종료되는 지점을 나타냅니다. 모든 주요 작업이 완료된 후 정리 작업을 수행하고, 필요에 따라 리소스를 해제하고 프로그램을 종료합니다.

간단한 예시로 메인루틴을 파이썬코드로 구현해보겠습니다. 아래와 같이 input으로 이름, 나이를 입력받아 출력하는 프로그램입니다. 다른 모듈에서 호출되는 것 없이 main()함수가 호출되고 종료됩니다. 이 main함수에서 프로그램이 시작되고, 종료되기에, "프로그램의 흐름이 정의"되었다라고 말할 수 있습니다. 즉, main()이 메인루틴이 됩니다.

def main():
    print("프로그램 시작")

    # 사용자 입력 받기
    name = input("이름을 입력하세요: ")
    age = int(input("나이를 입력하세요: "))

    # 정보 출력
    print("입력된 정보:")
    print("이름:", name)
    print("나이:", age)

    # 종료 메시지 출력
    print("프로그램 종료")

if __name__ == "__main__":
    main()

 

서브루틴(Subroutine): 메인루틴이나 서브루틴에서 호출되는 함수


서브루틴은 다른 프로그램에서 호출되는 (=피호출)되는 함수를 의미합니다. 예는 아래와 같습니다. 위의 예시와 같이 main()이 호출되고 종료되기에 메인루틴은 main()이라는 함수입니다. 그리고 이 main()내에서 calculate_sum()이 호출되고 종료됩니다. 즉, main인 메인루틴에 의해서 호출되는 calculate_sum이 서브루틴입니다. 

def main():
    print("메인루틴 시작")
    result = calculate_sum(3, 4)
    print("결과:", result)
    print("메인루틴 종료")

def calculate_sum(a, b): # 서브루틴
    return a + b

if __name__ == "__main__":
    main()

시간에 흐름에 따라, 두 루틴을 시각화하면 아래와 같습니다. 메인루틴이 서브루틴을 호출하고, 그 동안 메인루틴은 서브루틴이 일을 다 처리할 때까지, 잠시 작업대기를 합니다. 그리고, 서브루틴의 일이 끝나면, 메인루틴으로 다시 작업이 넘어가서 메인루틴의 테스크가 종료됩니다.

Figure 1. 시간의 흐름에 따른 메인루틴-서브루틴

 

코루틴(Coroutine): "일시중지 후 재시작"이 가능한 함수


코루틴은 일시 중단되고 다시 시작될 수 있는 함수로, 일반적인 함수와 다르게 실행 중에 일시 중단되어 값을 주고 받을 수 있습니다. 이를 통해 비동기적인 작업이나 상태를 유지하는 작업을 효율적으로 처리할 수 있습니다. 특히, 코루틴은 서브루틴과 다르게 메인루틴으로 작업상태가 넘어가도, 코루틴내 내용을 잃지않고 유지할 수 있습니다. 그리고, 이런 메인루틴->코루틴, 코루틴->메인루틴의 작업상태를 넘겨 실행하는 단계를 "진입점"이라고합니다. 이런 진입점은 서브루틴은 "메인루틴->서브루틴"의 1개의 진입점을 가지지만, 코루틴은 여러 개의 진입점을 가집니다.

 

코루틴을 사용하려면, 코루틴의 성격을 정리해보겠습니다. 

  • 코루틴은 generator의 특별한 형태입니다. (generator도 데이터를 yield와 함께 하나씩 얻어올 수 있고, 그 다음 차례가 그대로 유지되기에 코루틴으로 취급합니다.)
  • 코루틴은 파이썬의 2.5부터 generator로 설명되지만, 실제로 async, await구문과 함께쓰려면 3.5의 이상부터 필요합니다.

그리고, 코루틴의 시작/값보내기/값얻기의 구현방법을 알아야합니다.

  • 코루틴의 시작: 코루틴은 보통 while True 구문을 이용해서 계속해서 일반적으로 코루틴의 시작을 알립니다.
  • 코루틴에 값보내기: 코루틴에 값을 보내는 방법은 코루틴객체.send(객체)로 외부의 값을 코루틴으로 전달합니다.
  • 코루틴 내에서 값 얻기: 이 값을 가져오기 위해서, `yield` 키워드를 이용하여 값을 얻어옵니다.

 

아래의 예시에서는 `searcher()`가 코루틴 함수입니다. 하나 씩 내용을 확인해보겠습니다.

  1. while구문으로 코루틴을 계속 실행하는 상태로 둡니다.
  2. while 구문이 이하가 도는 경우, yield구문에서 값이 올 때까지 기다립니다.
  3. next()가 호출되어야하는 이유는 제너레티너나 코루틴을 실행하고 첫번째 값을 얻기위해 호출합니다. 코루틴은 yield 표현식이 보이면 일시중단됩니다.
  4. 코루틴객체.send(객체)의 구문으로 코루틴에 값을 보내면, 그 보내는것과 동시에 코루틴이 실행됩니다. 다시 while True 구문에 의해서 yield 키워드가 등장하고, 코루틴이 일시중지됩니다. 이와 동시에 다시 메인루틴으로 작업이 옮겨갑니다.

 

def searcher():
    """
    검색어를 받아들이고 출력하는 코루틴
    """
    while True:
        query = yield  # 일시 중단하고 외부로부터 값을 받음
        print("검색어:", query)

# 코루틴 생성
search = searcher()
next(search)  # 코루틴 시작

# 코루틴에 값 전달
search.send("파이썬")
search.send("데이터 분석")

Figure2. 메인루틴-코루틴의 시간에 엔트리포인트 예

또, 다른 예시입니다. 코루틴은 일시정지를 함으로, 코루틴 내의 그 내용을 잃어버리지 않고 유지할 수 있다고 했습니다. 코루틴에서 average, count, total라는 변수를 계속 코루틴에서 들고 있고, send로 더할 때 마다, 코루틴의 변수가 변경되는 것을 확인할 수 있습니다.

def running_average():
    """
    입력된 숫자들의 평균을 계산하는 코루틴
    """
    total = 0
    count = 0
    average = None

    while True:
        value = yield average  # 새로운 값 받기
        total += value
        count += 1
        average = total / count

# 코루틴 생성
calc_average = running_average()
next(calc_average)  # 코루틴 시작

# 코루틴에 숫자 전달하고 평균 계산
print("평균:", calc_average.send(10))  # 출력: 10.0
print("평균:", calc_average.send(20))  # 출력: 15.0
print("평균:", calc_average.send(30))  # 출력: 20.0

 

 

코루틴을 이용한 async, await의 활용


asyncio을 사용해서 비동기적으로 작업을 수행할 수 있습니다. asyncio을 사용하기위해 대표적인 키워드 2개를 살펴보겠습니다.

      • async 키워드: async def로 함수를 정의하면 해당 함수는 코루틴 함수로 선언됩니다. 이는 함수가 비동기적으로 실행될 수 있음을 나타냅니다. 아래와 같이, async def 로 정의된 함수는 인자를 담아 호출하면, coroutine object로 정의됨을 알 수 있습니다.

    • await 키워드: await 키워드는 비동기 작업이 완료될 때까지 대기하고, 해당 작업의 결과를 반환합니다. await 있는 키워드가 실행되면, 이 라인의 구문이 실행되는 동안, "비동기적으로 메인루틴으로 잡이 넘어가고", "완료되면 메인루틴의 작업에서 다시 코루틴으로 작업이 넘어옵"니다.  Q. "async함수를 정의하면 꼭 await구문을 써야하냐?"할수도있는데, 꼭 사용하지 않아도 됩니다만, 비동기작업을 안쓸꺼면 굳이 async로 정의할 필요는 없을 것 같습니다
async def hello():
    print("안녕하세요, 코루틴!")

async def main():
    print("메인 루틴 시작")
    await hello()
    print("메인 루틴 종료")

asyncio.run(main())

 

아래의 예시를 보겠습니다.

  1. `async def`로 fetch_url이라는 함수를 비동기적인 함수로 정의했습니다. 
  2. `await` 키워드를 fetch_url내에 정의합니다. ` fetch_url `이 호출되고 await구문이 있는 라인이 실행되면 sleep이 실행되면서, 작업을 메인루틴으로 넘깁니다.
  3. 메인루틴이 실행중이다가, await구문이 완료되면 그 다음인 return으로 반환됩니다.
  4. `async main`에서는 await asyncio.gather로 비동기작업을 여러개로 구성할 때, 모든 작업이 완료될 때까지 대기해주는 함수입니다. 
import asyncio

async def fetch_url(url):
    print(f"URL 가져오는 중: {url}")
    await asyncio.sleep(1)  # 비동기적으로 1초 동안 대기
    print(f"URL 가져오기 완료: {url}")
    return f"Content of {url}"

async def main():
    tasks = [fetch_url("https://www.example.com"), fetch_url("https://www.google.com")]
    completed_tasks = await asyncio.gather(*tasks)
    print("모든 작업 완료:")
    for result in completed_tasks:
        print(result)

asyncio.run(main())

 

언제쓰면 유용한가? "네트워크 요청", "file I/O bound task", "데이터베이스 쿼리"


  1. 동시성 증가: 비동기 프로그래밍을 사용하면 여러 I/O 작업을 동시에 처리할 수 있습니다. 한 작업이 완료될 때까지 기다리지 않고 다음 작업을 시작할 수 있습니다.
  2. 응답 시간 개선: I/O 바운드 작업을 동시에 처리함으로써 전체 시스템의 응답 시간을 개선할 수 있습니다. 작업이 완료될 때까지 기다리는 동안 다른 작업을 수행하여 전체 처리 시간을 최소화할 수 있습니다.
  3. 자원 효율성: 비동기 프로그래밍을 사용하면 블로킹되는 대기 시간 동안 다른 작업을 수행할 수 있으므로 시스템 자원을 더 효율적으로 활용할 수 있습니다.
  4. 스케일링 용이성: 비동기 프로그래밍을 사용하면 더 많은 동시 요청을 처리할 수 있으며, 이로써 시스템의 스케일링이 용이해집니다.
반응형

+ Recent posts