<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>완두콩</title>
    <link>https://suwani.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 12 Aug 2026 10:46:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ssuwani</managingEditor>
    <image>
      <title>완두콩</title>
      <url>https://tistory1.daumcdn.net/tistory/4063725/attach/6ada8b386eb1419284663620be2dcf99</url>
      <link>https://suwani.tistory.com</link>
    </image>
    <item>
      <title>Github Actions로 Auto Tagging 하기</title>
      <link>https://suwani.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 버전 관리는 매우 중요합니다. 해당 버전을 기준으로 배포하고 관리하면 운영상의 혼란을 줄일 수 있습니다.&lt;br /&gt;Github Actions를 사용해서 배포 시 자동으로 태그를 업데이트 하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Auto Tagging 이 필요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 버전 관리를 수동으로 하다 보면 다음과 같은 문제들이 발생할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전 태그 생성을 깜빡하는 경우&lt;/li&gt;
&lt;li&gt;버전 규칙이 일관되지 않는 경우&lt;/li&gt;
&lt;li&gt;여러 개발자가 각자 다른 방식으로 태그를 생성하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 단순 Latest로 배포하는 것은 현재 버전을 파악하기 어렵고 ArgoCD와 같은 툴을 사용한다면 자동으로 배포되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들을 해결하기 위해 Github Actions를 통한 자동화된 태깅 시스템을 구축할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Semantic Versioning&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 버전 번호를 체계적으로 관리하기 위한 방법으로, MAJOR.MINOR.PATCH 형식을 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;버전 규칙&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 숫자는 다음과 같은 의미를 가집니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAJOR: 이전 버전과 호환되지 않는 변경사항 (1.0.0 &amp;rarr; 2.0.0)&lt;/li&gt;
&lt;li&gt;MINOR: 이전 버전과 호환되는 새로운 기능 추가 (1.0.0 &amp;rarr; 1.1.0)&lt;/li&gt;
&lt;li&gt;PATCH: 이전 버전과 호환되는 버그 수정 (1.0.0 &amp;rarr; 1.0.1)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 버전이 v1.2.3일 때:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버그 수정: v1.2.3 &amp;rarr; v1.2.4 (PATCH)&lt;/li&gt;
&lt;li&gt;새로운 기능: v1.2.3 &amp;rarr; v1.3.0 (MINOR)&lt;/li&gt;
&lt;li&gt;호환성이 깨지는 변경: v1.2.3 &amp;rarr; v2.0.0 (MAJOR)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 명확한 버전 관리를 통해 프로젝트의 변경사항을 쉽게 파악하고 관리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github Actions 구현&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNAmBo/btsLbS9ofe4/TNGXqUpKJmRkeVkKoGP8x0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNAmBo/btsLbS9ofe4/TNGXqUpKJmRkeVkKoGP8x0/img.png&quot; data-alt=&quot;Workflow의 실행 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNAmBo/btsLbS9ofe4/TNGXqUpKJmRkeVkKoGP8x0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNAmBo%2FbtsLbS9ofe4%2FTNGXqUpKJmRkeVkKoGP8x0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;452&quot; height=&quot;365&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Workflow의 실행 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진에서 설명한 것처럼 Workflow는 아래와 같은 순서로 수행됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Workflow를 실행할 때 버전 타입을 사용자가 지정합니다. (기본값 Patch)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: Docker Build and Push

on:
  # 수동 실행을 위한 workflow_dispatch 추가
  workflow_dispatch:
    inputs:
      version_type:
        description: 'Version upgrade type (major/minor/patch)'
        required: true
        default: 'patch'
        type: choice
        options:
        - major
        - minor
        - patch
  # 자동으로 트리거된 경우 patch 버전 배포 (브랜치 전략에 따라 삭제 혹은 브랜치 변경)
  push:
    branches: [ main ]&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;레포지토리로부터 태그를 가져옵니다. (기본값 v0.0.0)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- name: Get latest tag
  id: get_latest_tag
  run: |
    latest_tag=$(git describe --tags --abbrev=0 2&amp;gt;/dev/null || echo &quot;v0.0.0&quot;)
    echo &quot;latest_tag=${latest_tag}&quot; &amp;gt;&amp;gt; $GITHUB_ENV&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;버전을 업데이트합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시맨틱 버저닝(MAJOR.MINOR.PATCH) 규칙에 따라 버전을 업그레이드합니다. 그리고 이를 태그로 생성합니다. (git push)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;- name: Determine version bump
  id: semver
  run: |
    if [ &quot;${{ github.event_name }}&quot; = &quot;workflow_dispatch&quot; ]; then
      bump_type=&quot;${{ github.event.inputs.version_type }}&quot;
    else
      bump_type=&quot;patch&quot;
    fi

    # 현재 버전에서 새 버전 계산
    current_version=${latest_tag#v}
    IFS='.' read -r major minor patch &amp;lt;&amp;lt;&amp;lt; &quot;$current_version&quot;

    case $bump_type in
      major)
        new_version=&quot;$((major + 1)).0.0&quot;
        ;;
      minor)
        new_version=&quot;${major}.$((minor + 1)).0&quot;
        ;;
      patch)
        new_version=&quot;${major}.${minor}.$((patch + 1))&quot;
        ;;
    esac

    echo &quot;new_version=${new_version}&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
    echo &quot;New version will be: ${new_version}&quot;

- name: Create Git tag
  run: |
    git tag v${{ needs.version.outputs.new_version }}
    git push origin v${{ needs.version.outputs.new_version }}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이를 필요한 곳에 정의하면 됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도커 빌드 시 태그를 지정하는 예시 (도커 로그인 중략)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;- name: Build and push Docker image
  uses: docker/build-push-action@v4
  with:
    context: .
    push: true
    tags: |
      ${{ secrets.DOCKERHUB_USERNAME }}/semantic-versioning:latest
      ${{ secrets.DOCKERHUB_USERNAME }}/semantic-versioning:v${{ needs.version.outputs.new_version }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ArgoCD 배포를 위해 values.yaml 파일에 버전을 지정하는 예시&lt;/b&gt; (ArgoCD 애플리케이션 또한 동일 레포지토리에 있다고 가정)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- name: Update Version in values.yaml
  run: |
    cd deploy
    # yq를 사용하여 values.yaml 업데이트
    yq eval &quot;.image.tag = \&quot;v${{ needs.version.outputs.new_version }}\&quot;&quot; -i values.yaml

- name: Commit and Push Changes
  run: |
    cd deploy
    git config --global user.name 'github-actions[bot]'
    git config --global user.email 'github-actions[bot]@users.noreply.github.com'
    git add .
    git commit -m &quot;chore: update image tag to v${{ needs.version.outputs.new_version }}&quot;
    git push&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Git Tag 업데이트&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 된 태그를 Repository에 반영합니다. 이를 위해선 Actions에 권한을 부여해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Settings -&amp;gt; Actions -&amp;gt; General -&amp;gt; Read and write permissions -&amp;gt; 체크&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;- name: Create Git tag
  run: |
    git tag v${{ needs.version.outputs.new_version }}
    git push origin v${{ needs.version.outputs.new_version }}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 GitHub Actions를 통해 버전 관리를 자동화하면 다음과 같은 이점이 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일관된 버전 관리: Semantic Versioning 규칙에 따라 자동으로 버전이 관리됩니다.&lt;/li&gt;
&lt;li&gt;배포 자동화: 새로운 버전이 생성되면 자동으로 Docker 이미지가 빌드되고 ArgoCD를 통해 배포됩니다.&lt;/li&gt;
&lt;li&gt;실수 방지: 수동으로 버전을 관리할 때 발생할 수 있는 실수를 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;시간 절약: 반복적인 버전 관리 작업을 자동화하여 개발자가 더 중요한 작업에 집중할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시를 참고하여 프로젝트에 맞게 수정하여 사용하면 좋겠습니다~! 해당 코드는 &lt;a href=&quot;https://github.com/Ssuwani/semantic-versioning&quot;&gt;https://github.com/Ssuwani/semantic-versioning&lt;/a&gt; 에 있습니다.&lt;/p&gt;</description>
      <category>auto tagging</category>
      <category>CI/CD</category>
      <category>Github Actions</category>
      <category>Semantic Versioning</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/189</guid>
      <comments>https://suwani.tistory.com/189#entry189comment</comments>
      <pubDate>Sat, 7 Dec 2024 12:41:00 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI에서 JWT로 사용자 회원가입/로그인 구현하기</title>
      <link>https://suwani.tistory.com/188</link>
      <description>&lt;p&gt;오늘은 FastAPI에서 회원가입과 로그인 기능을 구현해보려합니다. 대부분의 프로젝트에서 필요한 기능임에도 실제로 구현해 본 경험이 없어 이번 기회에 구현해보았습니다. 피드백 주시면 감사드리겠습니다.&lt;/p&gt;
&lt;p&gt;코드는 &lt;a href=&quot;https://github.com/Ssuwani/fastapi-jwt&quot;&gt;https://github.com/Ssuwani/fastapi-jwt&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;아젠다&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;구현할 내용 Sequence Diagram으로 이해하기&lt;/li&gt;
&lt;li&gt;회원가입 구현하기&lt;/li&gt;
&lt;li&gt;로그인 구현하기&lt;/li&gt;
&lt;li&gt;내 정보 조회 구현하기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. 구현할 내용 Sequence Diagram으로 이해하기&lt;/h2&gt;
&lt;p&gt;먼저 그림으로 간단히 오늘 구현할 내용을 나타내 보았습니다.&lt;br&gt;조금 복잡해 보일 수 있지만 클라이언트로부터 회원가입, 로그인, 내 정보 조회 요청이 들어왔을 때의 동작이고 따라가다보면 어렵지 않게 따라갈 수 있을 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant 클라이언트
    participant FastAPI
    participant DB

    %% 회원가입 프로세스
    rect rgb(191, 223, 255)
    Note over 클라이언트,DB: 회원가입 (/auth/signup)
    클라이언트-&amp;gt;&amp;gt;+FastAPI: POST /auth/signup
    Note over 클라이언트,FastAPI: {email, password, username}
    FastAPI-&amp;gt;&amp;gt;DB: 이메일 중복 확인
    DB--&amp;gt;&amp;gt;FastAPI: 결과 반환
    alt 이메일 중복
        FastAPI--&amp;gt;&amp;gt;클라이언트: 400 Bad Request
    else 이메일 사용 가능
        FastAPI-&amp;gt;&amp;gt;FastAPI: 비밀번호 해싱 (bcrypt)
        FastAPI-&amp;gt;&amp;gt;DB: 사용자 저장
        FastAPI--&amp;gt;&amp;gt;-클라이언트: 200 OK (id, email, username)
    end
    end

    %% 로그인 프로세스
    rect rgb(255, 230, 204)
    Note over 클라이언트,DB: 로그인 (/auth/login)
    클라이언트-&amp;gt;&amp;gt;+FastAPI: POST /auth/login
    Note over 클라이언트,FastAPI: {email, password}
    FastAPI-&amp;gt;&amp;gt;DB: 사용자 조회
    DB--&amp;gt;&amp;gt;FastAPI: 사용자 정보
    alt 인증 실패
        FastAPI--&amp;gt;&amp;gt;클라이언트: 401 Unauthorized
    else 인증 성공
        FastAPI-&amp;gt;&amp;gt;FastAPI: JWT 토큰 생성 (30분)
        FastAPI--&amp;gt;&amp;gt;-클라이언트: 200 OK (access_token)
    end
    end

    %% 내 정보 조회 프로세스
    rect rgb(204, 255, 204)
    Note over 클라이언트,DB: 내 정보 조회 (/auth/me)
    클라이언트-&amp;gt;&amp;gt;+FastAPI: GET /auth/me
    Note over 클라이언트,FastAPI: Bearer {token}
    alt 토큰 유효
        FastAPI-&amp;gt;&amp;gt;DB: 사용자 조회
        DB--&amp;gt;&amp;gt;FastAPI: 사용자 정보
        FastAPI--&amp;gt;&amp;gt;-클라이언트: 200 OK (사용자 정보)
    else 토큰 무효
        FastAPI--&amp;gt;&amp;gt;클라이언트: 401 Unauthorized
    end
    end&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 회원가입 구현하기&lt;/h2&gt;
&lt;p&gt;회원이라는 건 모든 서비스에 필요한 것은 아닙니다. 하지만, 사용자별로 맞춤형 서비스를 제공하고 관리하기 위해서는 &amp;quot;회원&amp;quot;을 관리할 수 있어야 합니다.&lt;/p&gt;
&lt;p&gt;또, 서비스에서는 관리의 관점에서 사용자 별로 서로 다른 권한을 부여할 수 있어야 합니다. 특정 사용자는 다른 사용자의 개인 정보를 조회할 수 있어선 안됩니다.&lt;/p&gt;
&lt;p&gt;지금부터 구현할 FastAPI 서버는 사용자 요청으로부터 회원가입 혹은 로그인 요청을 받게 됩니다. 그러면 서버는 이를 검증하고 중복 여부를 확인하고 문제가 없다면 데이터베이스에 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rOxhW/btsKKEr2KMn/tTdBDRKgOm2Cfg1zigNjP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rOxhW/btsKKEr2KMn/tTdBDRKgOm2Cfg1zigNjP1/img.png&quot; data-alt=&quot;회원가입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rOxhW/btsKKEr2KMn/tTdBDRKgOm2Cfg1zigNjP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrOxhW%2FbtsKKEr2KMn%2FtTdBDRKgOm2Cfg1zigNjP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;396&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;코드로 구현하면 아래와 같습니다. (그림에서 로직 일부는 생략되어 있습니다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@router.post(&amp;quot;/signup&amp;quot;, response_model=UserResponse)
async def signup(user: UserCreate, db: Session = Depends(get_db)):
    # 이메일 중복 확인
    db_user = db.query(User).filter(User.email == user.email).first()
    if db_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, 
            detail=&amp;quot;Email already registered&amp;quot;
        )

    # 비밀번호 해싱 및 사용자 생성
    hashed_password = get_password_hash(user.password)
    db_user = User(
        email=user.email,
        username=user.username,
        hashed_password=hashed_password
    )
    # 데이터베이스에 저장
    db.add(db_user)
    db.commit()
    return db_user&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 로그인 구현하기&lt;/h2&gt;
&lt;p&gt;로그인이라는 과정은 회원별로 서로 다른 정보를 제공하기 위한 과정입니다. A 사용자에게 B 사용자의 정보를 제공해선 안됩니다. 그리고 어떤 데이터를 조회할 때마다 매번 사용자 인증을 할 순 없습니다.&lt;/p&gt;
&lt;p&gt;로그인은 일반적으로 사용자 이메일, 패스워드를 통해 인증을 진행합니다. 클라이언트가 서버에 요청을 보낼때마다 이메일, 패스워드를 전달해준다면 서버 입장에선 좋겠지만 보안상 안전하지 않습니다.&lt;/p&gt;
&lt;p&gt;따라서, 한번의 로그인 과정을 거치면 서버는 특정 시간 동안 유효한 &lt;strong&gt;토큰&lt;/strong&gt;을 클라이언트에게 발급해줍니다. 그리고 이 토큰을 가지고 서버에 요청을 보내면 서버는 토큰을 검증하고 요청을 처리합니다.&lt;/p&gt;
&lt;p&gt;또 이게 가능한 이유는 토큰은 사용자 정보를 암호화한 정보입니다. 서버만이 이를 복호화할 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfWyFe/btsKKNoPC6P/Y8TzvipoKOdcMaBA31awVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfWyFe/btsKKNoPC6P/Y8TzvipoKOdcMaBA31awVK/img.png&quot; data-alt=&quot;로그인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfWyFe/btsKKNoPC6P/Y8TzvipoKOdcMaBA31awVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfWyFe%2FbtsKKNoPC6P%2FY8TzvipoKOdcMaBA31awVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;396&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;코드로 구현하면 아래와 같습니다. (그림에서 로직 일부는 생략되어 있습니다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 토큰 유효 시간

@router.post(&amp;quot;/login&amp;quot;, response_model=Token)
async def login(user_credentials: UserLogin, db: Session = Depends(get_db)):
    # 사용자 확인
    user = db.query(User).filter(User.email == user_credentials.email).first()
    if not user or not verify_password(user_credentials.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=&amp;quot;Incorrect email or password&amp;quot;
        )

    # 토큰 생성
    access_token = create_access_token(
        data={&amp;quot;sub&amp;quot;: user.email},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    return {&amp;quot;access_token&amp;quot;: access_token, &amp;quot;token_type&amp;quot;: &amp;quot;bearer&amp;quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가로 create_access_token 함수는 토큰을 생성하는 함수입니다. 데이터를 입력받아 expire 시간을 설정하고 토큰을 반환합니다.&lt;/p&gt;
&lt;p&gt;토큰은 데이터를 암호화한 하나의 string에 불과합니다. 암호화할 때 사용되는 SECRET_KEY가 노출된다면 누구든 토큰을 통해 복호화할 수 있기에 노출되어선 안됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;SECRET_KEY = &amp;quot;SECRET_KEY&amp;quot; # 노출되어선 안됩니다!! (환경변수로 관리)

def create_access_token(
    data: dict, expires_delta: Optional[timedelta] = timedelta(minutes=15)
):
    to_encode = data.copy()
    expire = datetime.now(tz=UTC) + expires_delta

    to_encode.update({&amp;quot;exp&amp;quot;: expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 내 정보 조회 구현하기&lt;/h2&gt;
&lt;p&gt;너무 당연하지만,, A가 내 정보 조회 요청 시 A 정보가 반환되어야 합니다. 단, 또 다시 로그인을 요구하면 안됩니다. 앞서 서버로부터 받은 토큰으로 요청을 보내면 서버는 토큰을 검증하고 요청을 처리합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x92Gw/btsKLt4sTbm/tQ2C3CVssk4gxNweUSNg8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x92Gw/btsKLt4sTbm/tQ2C3CVssk4gxNweUSNg8K/img.png&quot; data-alt=&quot;토큰으로 인증&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x92Gw/btsKLt4sTbm/tQ2C3CVssk4gxNweUSNg8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx92Gw%2FbtsKLt4sTbm%2FtQ2C3CVssk4gxNweUSNg8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;396&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;토큰으로 인증&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;코드로 구현하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;라우터:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@router.get(&amp;quot;/me&amp;quot;, response_model=UserResponse)
async def get_user_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;토큰 검증 및 사용자 조회:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def get_current_user(
    token: str = Depends(oauth2_scheme),  # Header에 포함된 토큰
    db: Session = Depends(get_db),
) -&amp;gt; User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=&amp;quot;인증 정보를 확인할 수 없습니다&amp;quot;,
        headers={&amp;quot;WWW-Authenticate&amp;quot;: &amp;quot;Bearer&amp;quot;},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email: str = payload.get(&amp;quot;sub&amp;quot;)
    except JWTError:
        raise credentials_exception

    # DB로부터 사용자 조회
    user = db.query(User).filter(User.email == email).first()
    if user is None:
        raise credentials_exception

    return user&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Python</category>
      <category>FastAPI</category>
      <category>JWT</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/188</guid>
      <comments>https://suwani.tistory.com/188#entry188comment</comments>
      <pubDate>Sat, 16 Nov 2024 17:37:18 +0900</pubDate>
    </item>
    <item>
      <title>Quantization, 적용 후 성능 비교하기 (Pytorch, Cifar10)</title>
      <link>https://suwani.tistory.com/187</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;글에 주의가 필요합니다.. 양자화를 실제로 현업에 적용해 본 경험이 없고 짧게 공부한 지식으로 작성되었습니다.&amp;nbsp;&lt;br /&gt;피드백은 환영입니다. 감사합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화의 목표는 성능을 유지하면서 모델을 가볍고 빠르게 만드는 것입니다. &lt;/p&gt;
&lt;p&gt;&lt;del&gt;이걸 왜 안 해?&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 곳&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 연산이 필요해: 빠른 응답이 필요한 곳 (latency를 낮춰야 할 때)&lt;/li&gt;
&lt;li&gt;모델이 가벼워야 해: 모바일 디바이스, 임베디드 시스템, 엣지 컴퓨팅 등 자원이 제한된 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 양자화의 개념을 간단히 이해한 후, Cifar 10 데이터셋을 분류하기 위한 모델을 학습한 뒤 양자화하고 성능을 비교해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lUfV7/btsKtjOHDKU/hVF5hGwv341nwSACac5vK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lUfV7/btsKtjOHDKU/hVF5hGwv341nwSACac5vK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lUfV7/btsKtjOHDKU/hVF5hGwv341nwSACac5vK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlUfV7%2FbtsKtjOHDKU%2FhVF5hGwv341nwSACac5vK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1121&quot; height=&quot;500&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아젠다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Quantization 개념 이해하기&lt;/li&gt;
&lt;li&gt;Quantization 기법 알아보기 (PTQ와 QAT)&lt;/li&gt;
&lt;li&gt;모델 성능 비교하기&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Quantization 개념 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Dense Layer를 정의하면 레이어 내에 weight와 bias 값들은 어떠한 값을 가지고 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfymRt/btsKsADiF7p/FMIDktQkXDe3K56nU8l94k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfymRt/btsKsADiF7p/FMIDktQkXDe3K56nU8l94k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfymRt/btsKsADiF7p/FMIDktQkXDe3K56nU8l94k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfymRt%2FbtsKsADiF7p%2FFMIDktQkXDe3K56nU8l94k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1696&quot; height=&quot;832&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각은 &lt;code&gt;torch.float32&lt;/code&gt; 타입을 가지고 있는 것을 확인할 수 있습니다. 이는&lt;code&gt;torch.randn&lt;/code&gt; 함수의 dtype에서의 기본형과 같습니다. 이는 각 4바이트를 차지하는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화는 &quot;이 4바이트라는 큰 크기를 갖는 텐서를 더 작은 크기를 갖는 자료형으로 변경해서 필요로 하는 메모리를 줄이는 것&quot;입니다.&lt;br /&gt;&lt;s&gt;그러면 당연히 연산 속도도 빨라질 것입니다.&amp;nbsp;&lt;br /&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;더 작은 크기를 갖는 자료형으로 변경하면 &quot;당연히&quot; 연산 속도도 빨라질 것이라 생각했습니다.&lt;br /&gt;=&amp;gt; &lt;b&gt;FP16 -&amp;gt; FP8과 같은 형태에서는 커널 구현에 따라 연산 속도가 오히려 느린 경우도 생길 수 있다고 피드백을 받아 수정합니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;텐서에 대해 양자화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 텐서에 대해 1바이트의 크기를 갖는 &lt;code&gt;torch.qint8&lt;/code&gt; 형으로 양자화를 위해선 아래 코드로 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;torch.quantize_per_tensor(tensor, scale=1.0, zero_point=0, dtype=torch.qint8)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EOH5w/btsKswHRzfW/xvKl2tJwIUrQMjkzzuwX6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EOH5w/btsKswHRzfW/xvKl2tJwIUrQMjkzzuwX6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EOH5w/btsKswHRzfW/xvKl2tJwIUrQMjkzzuwX6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEOH5w%2FbtsKswHRzfW%2FxvKl2tJwIUrQMjkzzuwX6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2216&quot; height=&quot;686&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화를 통해 텐서가 갖는 크기가 줄었습니다. 그리고 가지고 있던 의미도 잃었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모델에 대해 양자화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, 모델을 대상으로 양자화를 수행해 봅시다. 간단한 모델을 만들고 크기를 확인해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UPEM0/btsKtJswQU2/Vp7lbk17fON0KFgyUAVslk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UPEM0/btsKtJswQU2/Vp7lbk17fON0KFgyUAVslk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UPEM0/btsKtJswQU2/Vp7lbk17fON0KFgyUAVslk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUPEM0%2FbtsKtJswQU2%2FVp7lbk17fON0KFgyUAVslk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1706&quot; height=&quot;672&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100 * 100 크기의 텐서를 정의했습니다. 그리고 각각은 4바이트를 차지합니다. 따라서 약 40,000 바이트를 차지하네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 텐서 변환과 동일하게 &lt;code&gt;torch.qint8&lt;/code&gt; 타입으로 변경해 보겠습니다. 4바이트를 1바이트로 줄일 수 있습니다.&lt;br /&gt;따라서 변환 시 아래와 같이 모델 크기가 줄어들 것을 기대합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100 * 100 * 1 = 10,000 바이트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;torch.quantization.quantize_dynamic(model, dtype=torch.qint8)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdFnws/btsKsmFkkaO/aKnk0N3uuSln61KOkhYqw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdFnws/btsKsmFkkaO/aKnk0N3uuSln61KOkhYqw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdFnws/btsKsmFkkaO/aKnk0N3uuSln61KOkhYqw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdFnws%2FbtsKsmFkkaO%2FaKnk0N3uuSln61KOkhYqw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2216&quot; height=&quot;734&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 텐서의 형변환을 통해 모델을 가볍고 빠르게 만드는 테크닉이 양자화입니다.&lt;br /&gt;그런데 여기서 눈치챌 수 있듯이 양자화는 모델의 성능을 떨어뜨릴 수 있습니다. 이에 주의가 필요하고 이를 최소화하기 위한 방법론 또한 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Quantization 기법 알아보기 (PTQ와 QAT)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화에는 크게 PTQ(Post-Training Quantization) 와 QAT(Quantization-Aware Training) 두 가지 방법이 있습니다.&lt;br /&gt;PTQ는 모델 학습이 완료된 후, 추가적인 훈련 없이 양자화를 수행하는 방법입니다. 그중 PTQ 방법론 중에서도 Dynamic과 Static 2가지로 나뉩니다.&lt;br /&gt;QAT는 학습 과정에서 양자화의 영향을 고려하여 훈련하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PTQ의 2가지 방법론 그리고 QAT까지 총 3가지 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. Dynamic Quantization (PTQ)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 끝난 모델의 가중치(weights)를 양자화합니다. 그리고 활성화(Activation)은 입력에 따라 동적으로 변환됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uvaif/btsKDSCuQHV/Q59rOtQa4ei3ezbqq3XcCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uvaif/btsKDSCuQHV/Q59rOtQa4ei3ezbqq3XcCK/img.png&quot; data-alt=&quot;Weight는 입력전에 미리 양자화 &amp;amp;amp; Activation은 입력에 따라 동적으로 조절 (https://christianjmills.com/posts/cuda-mode-notes/lecture-007/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uvaif/btsKDSCuQHV/Q59rOtQa4ei3ezbqq3XcCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuvaif%2FbtsKDSCuQHV%2FQ59rOtQa4ei3ezbqq3XcCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;482&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Weight는 입력전에 미리 양자화 &amp;amp; Activation은 입력에 따라 동적으로 조절 (https://christianjmills.com/posts/cuda-mode-notes/lecture-007/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 통해 어렵지 않게 변환할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730449413839&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;torch.backends.quantized.engine = &quot;qnnpack&quot;
dynamic_quantized_model = torch.quantization.quantize_dynamic(model, dtype=torch.qint8)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. Static Quantization (PTQ)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 모든 연산에 대해 양자화를 수행하고, 특히 모델 실행 전에 데이터 분포를 기반으로 scale과 zero_point 값을 미리 설정하여 최적화합니다.&lt;br /&gt;모델 변환 전에 Calibration 작업이 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# Static Quantization 적용
model.qconfig = torch.quantization.get_default_qconfig(&quot;qnnpack&quot;)
model_static_quantized = torch.quantization.prepare(model, inplace=False)

# Calibration 과정
num_calibration_samples = 2000  # 권장되는 샘플 수: 1000-2000
with torch.no_grad():
    for i, (images, _) in enumerate(trainloader):
        if i * 64 &amp;gt;= num_calibration_samples:  # 64는 batch size
            break
        model_static_quantized(images)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Note: Calibration 과정에서 권장되는 샘플 수는 약 1000-2000개입니다. 이를 통해 최적의 Scale/Offset을 찾아 모델 성능을 극대화할 수 있습니다.  참고문서 &lt;a href=&quot;https://quic.github.io/aimet-pages/AimetDocs/user_guide/quantization_sim.html#quantsim-workflow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AIMET&lt;/a&gt;&amp;nbsp;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. Quantization-Aware Training (QAT)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QAT는 학습 과정에서 양자화의 영향을 고려하여 훈련하는 방법입니다. 즉, 양자화된 모델의 성능 저하를 줄이기 위해 학습 전에 모델을 양자화를 수행합니다. 이 방식은 양자화로 인한 성능 손실을 최소화할 수 있어 성능에 민감한 환경에서 많이 사용됩니다. 다만, 학습 비용이 증가하고 구현이 다소 복잡할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730449375243&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;qat_model = CIFAR10Classifier()
qat_model.qconfig = torch.quantization.get_default_qat_qconfig(&quot;qnnpack&quot;)
torch.quantization.prepare_qat(qat_model, inplace=True)

# QAT 훈련
qat_model = train_model(qat_model, trainloader, epochs=5)
torch.quantization.convert(qat_model, inplace=True)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 모델 성능 비교하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cifar 10 데이터셋을 이용해 간단한 분류 모델을 학습한 후, 앞서 설명한 3가지 방식의 성능을 비교해 보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화를 적용하지 않은 원래 모델과 Dynamic Quantization, Static Quantization, QAT를 적용한 모델들의 성능을 비교해 보면 다음과 같습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;정확도 (%)&lt;/th&gt;
&lt;th&gt;모델 크기 (MB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본 모델&lt;/td&gt;
&lt;td&gt;61.95&lt;/td&gt;
&lt;td&gt;2.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic Quantization&lt;/td&gt;
&lt;td&gt;61.91&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static Quantization&lt;/td&gt;
&lt;td&gt;61.87&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantization-Aware Training&lt;/td&gt;
&lt;td&gt;60.95&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상한 대로 양자화를 적용한 모델은 일반 모델에 비해 크기가 크게 줄어들었고 정확도에서 약간의 손실이 발생했습니다. Trade-Off 관계를 잘 이해하고 적절한 양자화 방법을 선택하는 것이 중요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화는 모델의 경량화와 속도 향상이라는 장점을 제공하는 중요한 기술입니다. 특히, 자원이 제한된 환경에서 추론을 수행할 때 유용합니다. 이번 글에서 사용했던 코드는 여기에서 노트북 파일로 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Ssuwani/pytorch-quantization/blob/main/main.ipynb&quot;&gt;https://github.com/Ssuwani/pytorch-quantization/blob/main/main.ipynb&lt;/a&gt;&lt;/p&gt;</description>
      <category>MLOps</category>
      <category>pytorch</category>
      <category>Quantization</category>
      <category>양자화</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/187</guid>
      <comments>https://suwani.tistory.com/187#entry187comment</comments>
      <pubDate>Fri, 1 Nov 2024 17:11:24 +0900</pubDate>
    </item>
    <item>
      <title>Airflow에서 Deferrable Operator란?</title>
      <link>https://suwani.tistory.com/186</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;아젠다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Deferrable의 의미&lt;/li&gt;
&lt;li&gt;Deferrable Operator &lt;span&gt;사용&lt;/span&gt; &lt;span&gt;및&lt;/span&gt; &lt;span&gt;작성&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Deferrable의 의미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Deferrable&amp;rdquo;은 &amp;ldquo;연기할 수 있는&amp;rdquo; 또는 &amp;ldquo;지연 가능한&amp;rdquo;이라는 의미를 갖고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존의 Operator: 작업이 대기 상태에 있을 때도 워커를 점유하면서 리소스를 사용하지만,&lt;/li&gt;
&lt;li&gt;Deferrable Operator: 작업을 중지하고 필요할 때 다시 시작할 수 있어 워커 리소스를 절약합니다&lt;br /&gt;이를 통해 시스템의 리소스 효율성을 높이고, 더 많은 작업을 동시에 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow에서 특정 Task를 수행한다는 것은 기본적으로 워커의 슬롯을 사용하는 것을 의미합니다. &lt;br /&gt;하지만, Deferrable Operator는 워커에서 동작하는 것이 아닌 Triggerer라는 워커에서 동작합니다.&lt;br /&gt;Triggerer 워커는 Airflow의 Optional Component로 Airflow의 구성에 꼭 필요한 컴포넌트는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cclIsK/btsKkgdV6ts/qRcO8T1YQsjHe3w9WGMUj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cclIsK/btsKkgdV6ts/qRcO8T1YQsjHe3w9WGMUj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cclIsK/btsKkgdV6ts/qRcO8T1YQsjHe3w9WGMUj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcclIsK%2FbtsKkgdV6ts%2FqRcO8T1YQsjHe3w9WGMUj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;525&quot; height=&quot;560&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;1138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deferrable Operator를 사용해 워커에 영향을 주지 않도록 작성된 프로세스들이 비동기로 동작하기 위한 컴포넌트입니다. 따라서 Deferrable Operator를 사용하지 않는다면 낭비되는 리소스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 실험을 위해 GCP에서 제공하는 Managed Airflow인 Composer를 구성했습니다. 이때 아래 사진과 같이 Triggered도 함께 생성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3308&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lvQyb/btsKmpUvq9J/fHXZu7KPY8KZgdoPlPBJ7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lvQyb/btsKmpUvq9J/fHXZu7KPY8KZgdoPlPBJ7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lvQyb/btsKmpUvq9J/fHXZu7KPY8KZgdoPlPBJ7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlvQyb%2FbtsKmpUvq9J%2FfHXZu7KPY8KZgdoPlPBJ7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3308&quot; height=&quot;260&quot; data-origin-width=&quot;3308&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Deferrable Operator 사용 및 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow에서 기본적으로 제공하는 Operator 중에도 Deferrable Operator가 있습니다.&lt;br /&gt;TimeSensorAsync, SqsSensor Operator와 같이 비동기로 동작할 수 있는 Sensor Operator들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TimeSensor&lt;b&gt;Async&lt;/b&gt;의 경우 이름에서 알 수 있듯이 비동기로만 동작합니다. (&amp;lt;-&amp;gt; TimeSensor는 동기로 동작합니다.)&lt;/li&gt;
&lt;li&gt;SqsSensor의 경우 동기 혹은 비동기를 선택할 수 있습니다. (argument 중 deferrable=True 혹은 False로 설정할 수 있습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeSensorAsync를 사용해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import datetime

from airflow import DAG
from airflow.sensors.time_sensor import TimeSensorAsync


with DAG(
    &quot;deferrable_dag&quot;,
    start_date=datetime.datetime(2024, 1, 1),
    catchup=False,
) as dag:
    t1 = TimeSensorAsync(
        task_id=&quot;wait_5_minutes&quot;,
        target_time=(
            datetime.datetime.now(tz=datetime.timezone.utc)
            + datetime.timedelta(minutes=5)
        ).time(),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 5분간 대기하는 Task입니다. 해당 코드가 Sync로 동작한다면 워커의 Slot을 점유하면서 5분간 기다릴 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;b&gt;동기로 동작하는&lt;/b&gt; TimeSensor의 코드를 일부 가져왔습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class TimeSensor(BaseSensorOperator):
    def __init__(self, *, target_time: datetime.time, **kwargs) -&amp;gt; None:
        super().__init__(**kwargs)
        self.target_time = target_time

    def poke(self, context: Context) -&amp;gt; bool:
        self.log.info(&quot;Checking if the time (%s) has come&quot;, self.target_time)
        return timezone.make_naive(timezone.utcnow(), self.dag.timezone).time() &amp;gt; self.target_time

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;poke 메서드는 현재 시간과 target_time을 비교해서 True 혹은 False를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;airflow는 Operator는 execute 메서드를 통해 작업을 수행하는데 BaseSensorOperator의 execute 메서드의 일부는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def execute(self, context: Context) -&amp;gt; Any:
    while True:
        try:
            poke_return = self.poke(context)

        if poke_return:
            break&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한루프를 돌면서 앞서 구현된 poke 메서드를 실행해 특정 시간이 되었는지 확인합니다. -&amp;gt; &lt;b&gt;리소스를 점유 ❗&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, TimeSensorAsync는 워커 노드에서 동작하는 것이 아닌 Triggered에서 실행되며 비동기로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeSensorAsync의 execute 메서드부터 천천히 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;    def execute(self, context: Context) -&amp;gt; NoReturn:
        self.defer(
            trigger=DateTimeTrigger(moment=self.target_datetime, end_from_trigger=self.end_from_trigger),
            method_name=&quot;execute_complete&quot;,
        )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self.defer 메서드는 해당 Task를 deferred 상태로 변경하고 작업의 실행을 중단시킵니다. 인자로 넘긴 Trigger가 특정 이벤트를 발생시킬 때까지 대기합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;1384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwR8Gq/btsKj2mKmkH/0sS2cscPRrGH0obPHVJUY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwR8Gq/btsKj2mKmkH/0sS2cscPRrGH0obPHVJUY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwR8Gq/btsKj2mKmkH/0sS2cscPRrGH0obPHVJUY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwR8Gq%2FbtsKj2mKmkH%2F0sS2cscPRrGH0obPHVJUY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2316&quot; height=&quot;1384&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;1384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 정의된 DateTimeTrigger는 async def run으로 비동기 메서드가 정의되었고 이를 Triggered에서 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;    async def run(self) -&amp;gt; AsyncIterator[TriggerEvent]:
        for step in 3600, 60, 10:
            seconds_remaining = (self.moment - pendulum.instance(timezone.utcnow())).total_seconds()
            while seconds_remaining &amp;gt; 2 * step:
                self.log.info(&quot;%d seconds remaining; sleeping %s seconds&quot;, seconds_remaining, step)
                await asyncio.sleep(step)
                seconds_remaining = (self.moment - pendulum.instance(timezone.utcnow())).total_seconds()
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시간임을 알기 위해선 시간을 계속해서 검사할 수밖에 없긴 하지만, 지수 백오프 기법을 사용해 대기 시간을 줄이고 있고 해당 대기 또한 비동기로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드는 Custom Deferrable Operator 정의 시 직접 구현해줘야 하는 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Triggered 프로세스에서 실행을 확인하기 위해 해당 Pod의 로그를 확인해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 ex) &lt;code&gt;kubectl logs -f -n composer-2-9-7-airflow-2-9-3-1a3758bc  airflow-triggerer-7bcc7bb8f-r56lb&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3656&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SzrsS/btsKkGwK6zC/YbnVqthtG73LpLaeEGrs11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SzrsS/btsKkGwK6zC/YbnVqthtG73LpLaeEGrs11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SzrsS/btsKkGwK6zC/YbnVqthtG73LpLaeEGrs11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSzrsS%2FbtsKkGwK6zC%2FYbnVqthtG73LpLaeEGrs11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3656&quot; height=&quot;522&quot; data-origin-width=&quot;3656&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 더 알고 있으면 좋은 점은 Custom Deferrable Operator를 작성할 때 Triggered Pod에서는 DAG Folder를 마운트 하지 않는다는 점입니다. 따라서 정의한 Trigger 클래스에서 Dag folder 내의 파일을 참조한다면 참조 에러가 발생합니다. 이는 Triggered에서 독립적으로 일관성 있게 작업을 수행하기 위한 제약입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;airflow에서 SqsSensor를 사용하면서 궁금했던 Deferrable에 대해 알아보았습니다. 사실 위 내용을 작성하면서도 정확하게 이해하지 못하고 있는 부분은 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;def serialize 메서드를 통해 직렬화되어 Triggered에 전달될 텐데 어떻게 전달되는지?&lt;/li&gt;
&lt;li&gt;worker에서도 비동기로 실행되게끔 할 수 있을 텐데, 꼭 Triggered로 분리할 필요가 있는지?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 제가 잘못 이해하고 있는 부분이나 알려주실 내용 있으시면 댓글 부탁드립니다.  &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;References&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/deferring.html#writing-deferrable-operators&quot;&gt;https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/deferring.html#writing-deferrable-operators&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>데이터 엔지니어링</category>
      <category>Airflow</category>
      <category>Deferrable</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/186</guid>
      <comments>https://suwani.tistory.com/186#entry186comment</comments>
      <pubDate>Sun, 27 Oct 2024 16:08:58 +0900</pubDate>
    </item>
    <item>
      <title>데이터 드리프트 감지하기 (w/ BentoML, Evidently AI, Fluent Bit)</title>
      <link>https://suwani.tistory.com/185</link>
      <description>&lt;h1&gt;BentoML Data Drift Detection&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 드리프트는 훈련에 사용된 데이터와 실제로 모델이 적용되는 시점의 데이터 간 분포 차이가 발생하는 현상을 말합니다.&lt;br /&gt;이러한 분포의 차이는 모델 성능을 저하시킬 수 있습니다. 이에 데이터 드리프트를 감지하고 대응하는 것은 중요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqM9kv/btsKdpov5id/sRJgjtpBFD2DLp5BriohR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqM9kv/btsKdpov5id/sRJgjtpBFD2DLp5BriohR0/img.png&quot; data-alt=&quot;https://www.evidentlyai.com/ml-in-production/data-drift&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqM9kv/btsKdpov5id/sRJgjtpBFD2DLp5BriohR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqM9kv%2FbtsKdpov5id%2FsRJgjtpBFD2DLp5BriohR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;784&quot; height=&quot;450&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.evidentlyai.com/ml-in-production/data-drift&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아젠다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터 드리프트를 감지하는 방법&lt;/li&gt;
&lt;li&gt;BentoML로 인퍼런스 데이터 로깅&lt;/li&gt;
&lt;li&gt;Fluent Bit으로 로그 수집&lt;/li&gt;
&lt;li&gt;Kafka에 수집된 로그 전달&lt;/li&gt;
&lt;li&gt;Evidently AI로 데이터 드리프트 감지&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 데이터 드리프트를 감지하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 드리프트를 감지하는 데에는 여러 가지 방법이 존재합니다. 주요 접근 방식은 위 사진에서 알 수 있듯이 훈련 데이터(기준 데이터)와 인퍼런스 시점의 실시간 데이터를 비교하여 데이터 분포의 차이를 감지하는 것입니다.&lt;br /&gt;데이터를 어떻게 비교하고 드리프트를 감지할지에 따라 다양한 방법을 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 통계적 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KL Divergence&lt;br /&gt;직관적으로 두 확률 분포간에 차이를 측정하는 방법, 값이 클 수록 드리프트 多&lt;/li&gt;
&lt;li&gt;Jensen-Shannon Divergence&lt;br /&gt;KL Divergence의 단점을 보완한 방법, 양쪽 데이터 간의 차이를 고르게 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 Kolmogorov-Smirnov, Chisquare 등 다양한 방법이 존재합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 머신러닝 기반 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통계적 방법 외에도, 머신러닝을 활용한 데이터 드리프트 감지 방법도 존재합니다. 이 방법은 훈련 데이터와 인퍼런스 데이터를 하나의 데이터셋으로 결합한 후, 이 데이터를 분류하는 새로운 머신러닝 모델을 학습시키는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 Drift Detection Method, Adversarial Validation 와 같은 기법이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3. 그래서 어떻게?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tools&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 드리프트를 감지할 수 있는 다양한 툴이 존재합니다. TFDV, Evidently AI, Seldon Core 등이 있습니다.&lt;br /&gt;이 중 Evidently AI를 사용하여 데이터 드리프트를 감지하는 방법을 소개하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;How&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인퍼런스 서버로 들어오는 데이터를 모델 예측과 함께 데이터 드리프트를 감지할 수도 있을것입니다.&lt;br /&gt;하지만 이 방법은 모델 예측 성능에 영향을 미칠 수 있습니다. 따라서 인퍼런스 서버에서는 인퍼런스 데이터를 로깅하고 이를 수집하여 또 다른 서버에서 데이터 드리프트를 감지해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNydtt/btsKeJ0mEJB/ugQ50Zx6ZSq8AB2t3zpkW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNydtt/btsKeJ0mEJB/ugQ50Zx6ZSq8AB2t3zpkW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNydtt/btsKeJ0mEJB/ugQ50Zx6ZSq8AB2t3zpkW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNydtt%2FbtsKeJ0mEJB%2FugQ50Zx6ZSq8AB2t3zpkW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;411&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BentoML로 인퍼런스 데이터 로깅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BentoML은 &lt;a href=&quot;https://docs.bentoml.org/en/latest/guides/observability/monitoring-and-data-collection.html&quot;&gt;Monitoring을 위한 Data Collection&lt;/a&gt;을 제공합니다.&lt;br /&gt;&lt;i&gt;Cloud Monitoring Service 들을 지원하는 것 같은데,, 시도해보다 잘 안되네요&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 그래서 저는 기본적(type:&quot;default&quot;)으로 제공하는 log 파일에 로깅하는 방법을 사용하였습니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;@bentoml.api
def classify(self, input_series: np.ndarray) -&amp;gt; np.ndarray:
    with bentoml.monitor(self.service_name) as mon:
        mon.log(
            self.service_name,
            name=&quot;service_name&quot;,
            role=&quot;service_name&quot;,
            data_type=&quot;text&quot;,
        )
        mon.log(
            input_series,
            name=&quot;input_array&quot;,
            role=&quot;feature&quot;,
            data_type=&quot;numerical_sequence&quot;,
        )

        result = self.model.predict(input_series)

        mon.log(
            result, name=&quot;output_array&quot;, role=&quot;prediction&quot;, data_type=&quot;numerical&quot;
        )
        return result&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정의한 로그는 아래와 같이 JSON 형식으로 ./monitoring//data/data.*.log 파일에 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# ex) 예시 로그
{
    &quot;service_name&quot;: &quot;iris_classifier&quot;,
    &quot;input_array&quot;: &quot;[[ 0  0  0 18]]&quot;,
    &quot;output_array&quot;: &quot;[0]&quot;,
    &quot;timestamp&quot;: &quot;2024-10-21T15:41:13.201061&quot;,
    &quot;request_id&quot;: &quot;3707933027466346669&quot;,
    &quot;trace_id&quot;: &quot;12411806532643160228087324403276272721&quot;,
    &quot;taskName&quot;: null,
}

# 파일 저장 위치
monitoring/
└── iris_classifier
    ├── data
    │   └── data.1.log
    └── schema
        └── schema.1.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 파일의 위치는 Bento Service 정의 시 아래와 같이 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;@bentoml.service(
    name=&quot;iris_classifier&quot;,
    monitoring={
        &quot;enabled&quot;: True,
        &quot;type&quot;: &quot;default&quot;,
        &quot;options&quot;: {
            &quot;log_file_path&quot;: &quot;./monitoring&quot;, # 여기서 변경 가능, monitoring은 기본값
        },
    },
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Fluent Bit으로 로그 수집&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 과정에서 로깅한 데이터는 모니터렁 뿐 아니라 추후 모델 학습 과정에서도 사용할 수 있습니다.&lt;br /&gt;이에 Kafka 토픽에 저장해두려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 방법 중 &lt;a href=&quot;https://fluentbit.io/&quot;&gt;Fluent Bit&lt;/a&gt;을 사용하고자 합니다. (설치는 &lt;code&gt;brew install fluent-bit&lt;/code&gt;로 진행했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fluent Bit은 경량의 로그 및 데이터 수집 에이전트로, 다양한 소스에서 데이터를 수집하고 Kafka, S3, BigQuery 등으로 전달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT6iO1/btsKeD0kUPl/kbUAnNLLfEvukIWRBjDQlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT6iO1/btsKeD0kUPl/kbUAnNLLfEvukIWRBjDQlK/img.png&quot; data-alt=&quot;https://fluentbit.io/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT6iO1/btsKeD0kUPl/kbUAnNLLfEvukIWRBjDQlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT6iO1%2FbtsKeD0kUPl%2FkbUAnNLLfEvukIWRBjDQlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1046&quot; height=&quot;671&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://fluentbit.io/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 수집을 위한 파이프라인 구성은 아래와 같습니다. (&lt;i&gt;Kafka 설치는 &lt;a href=&quot;https://suwani.tistory.com/151&quot;&gt;여기&lt;/a&gt;를 따라서 로컬에 Docker-Compose로 설치했습니다.&lt;/i&gt;)\&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input: 로그 파일&lt;/li&gt;
&lt;li&gt;Output: Kafka Topic&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fluent bit은 기본적으로 .conf 파일에 파이프라인 설정을 정의하고 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# fluent-bit-data-collection.conf
[Service]
    Flush        5
    Daemon       Off
    Log_Level    info

[INPUT]
    Name         tail
    Path         ./monitoring/*/data/*.log # 경로 확인!! 
    Tag          my_json_log

[OUTPUT]
    Name          kafka
    Match         *
    Brokers       localhost:9092
    Topics        ml_logs # 토픽을 생성해주세요.
    Message_Key_Field service_name # 앞서 로깅 시 service_name 필드를 지정한 것을 메시지 Key로 지정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;fluent-bit -c fluent-bit-data-collection.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 로그는 아래와 같이 전달됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btFXCW/btsKeFX8XvY/rcmU2p2T5zJRtSNWoavdFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btFXCW/btsKeFX8XvY/rcmU2p2T5zJRtSNWoavdFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btFXCW/btsKeFX8XvY/rcmU2p2T5zJRtSNWoavdFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtFXCW%2FbtsKeFX8XvY%2FrcmU2p2T5zJRtSNWoavdFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1640&quot; height=&quot;442&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습한 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5112&quot; data-origin-height=&quot;2828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UKxyo/btsKeMJrL42/vSLxXwvPIy9PTJV3u8Ki81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UKxyo/btsKeMJrL42/vSLxXwvPIy9PTJV3u8Ki81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UKxyo/btsKeMJrL42/vSLxXwvPIy9PTJV3u8Ki81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUKxyo%2FbtsKeMJrL42%2FvSLxXwvPIy9PTJV3u8Ki81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5112&quot; height=&quot;2828&quot; data-origin-width=&quot;5112&quot; data-origin-height=&quot;2828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Kafka에 수집된 로그 전달&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽에 저장된 로그를 전달하는 방법은 여러가지가 있습니다. 생각하는 가장 쉬운 방법은 Kafka Connector를 사용하는 방법입니다.&lt;br /&gt;HTTP Sink Connector를 사용하면 API 서버에 손쉽게 메시지를 가지고 Request를 보낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기선 위 단계와 동일하게 Fluent Bit을 사용해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fluent Bit에선 앞서 Log file -&amp;gt; Kafka와 같은 파이프라인을 생성했었습니다.&lt;br /&gt;이번엔 Kafka -&amp;gt; API 서버로 파이프라인을 구성해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용가능한 &lt;a href=&quot;https://docs.fluentbit.io/manual/pipeline/inputs&quot;&gt;Input&lt;/a&gt; / &lt;a href=&quot;https://docs.fluentbit.io/manual/pipeline/outputs&quot;&gt;Output&lt;/a&gt; 컴포넌트를 확인해주세요.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# fluent-bit-data-sink.conf
[INPUT]
    Name            kafka
    Brokers         localhost:9092
    Topics          ml_logs
    Group_Id        test
    Tag             kafka_logs
    Format          json

[OUTPUT]
    Name            http
    Match           kafka_logs
    Host            localhost
    Port            5001
    URI             /validation
    Format          json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽에 메시지가 들어오면 localhost:5001/validation 경로로 POST로 요청이 전달됩니다.&lt;br /&gt;(5번에서 /validation 라우터를 구현해보겠습니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Evidently AI로 데이터 드리프트 감지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청을 처리할 수 있는 API를 FastAPI로 구현하고자 합니다.&lt;br /&gt;해당 앱은 /validation 라우터에서 POST 요청을 처리할 수 있어야하고 요청 받은 데이터를 통해 데이터 드리프트를 감지할 수 있어야 합니다.&lt;br /&gt;앱에서 필요한 것은 학습 시 사용한 데이터와 현재 인퍼런스 데이터입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Evidently AI를 통해 데이터 드리프트를 판단하는 코드를 먼저 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def is_drift(origin_data: pd.DataFrame, current_data: pd.DataFrame) -&amp;gt; bool:
    data_drift_report = Report(metrics=[DataDriftPreset()])
    data_drift_report.run(reference_data=origin_data, current_data=current_data)
    data_drift_result = data_drift_report.as_dict()
    dataset_drift = data_drift_result[&quot;metrics&quot;][0] # DatasetDriftMetric
    return dataset_drift[&quot;result&quot;][&quot;dataset_drift&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Evidently의 Report를 정의한 뒤 DataDriftPreset을 사용해서 드리프트 분석에 필요한 기본 메트릭을 설정합니다.&lt;br /&gt;이후 run 메서드를 통해 드리프트 분석을 진행압니다.&lt;br /&gt;코드는 origin_data로부터 current_data의 데이터 드리프트가 발생 했는지 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataDriftPreset은 두개의 메트릭으로 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DatasetDriftMetric: 데이터셋 전체의 드리프트 여부를 판단&lt;/li&gt;
&lt;li&gt;DataDriftTable: 각 컬럼별 드리프트 여부를 판단&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시코드는 데이터셋 전체의 드리프트 여부만을 판단해서 True 혹은 False를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI를 앱을 완성하면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# app.py
import pandas as pd
from evidently.metric_preset import DataDriftPreset
from evidently.report import Report
from fastapi import FastAPI, Request

app = FastAPI()

train_feature_dataset = pd.read_csv(&quot;train_feature_dataset.csv&quot;)


def is_drift(origin_data: pd.DataFrame, current_data: pd.DataFrame) -&amp;gt; bool:
    data_drift_report = Report(metrics=[DataDriftPreset()])
    data_drift_report.run(reference_data=origin_data, current_data=current_data)
    data_drift_result = data_drift_report.as_dict()
    dataset_drift = data_drift_result[&quot;metrics&quot;][0]
    return dataset_drift[&quot;result&quot;][&quot;dataset_drift&quot;]


@app.post(&quot;/validation&quot;)
async def validate_data(request: Request):
    data_list: list[dict] = await request.json()
    features = [item for data in data_list for item in data[&quot;payload&quot;][&quot;input_array&quot;]]
    feature_df = pd.DataFrame(features, columns=train_feature_dataset.columns)
    if is_drift(train_feature_dataset, feature_df):
        print(&quot;Dataset drift detected&quot;)
        # notification ex) Slack 알림
    else:
        print(&quot;Dataset drift not detected&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 직접 실습하면 아레 사진과 같이 데이터 드리프트 감지까지를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3836&quot; data-origin-height=&quot;2114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deDFWG/btsKdGwLY7W/flxeBnQBVqFA7naZ15azfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deDFWG/btsKdGwLY7W/flxeBnQBVqFA7naZ15azfK/img.png&quot; data-alt=&quot;순서가 고르지 못하네요 ..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deDFWG/btsKdGwLY7W/flxeBnQBVqFA7naZ15azfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeDFWG%2FbtsKdGwLY7W%2FflxeBnQBVqFA7naZ15azfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3836&quot; height=&quot;2114&quot; data-origin-width=&quot;3836&quot; data-origin-height=&quot;2114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;순서가 고르지 못하네요 ..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BentoML을 통해 Feature 로깅부터 FastAPI 앱을 통해 데이터 드리프트 감지까지 과정을 수행했습니다.&lt;br /&gt;이 과정에서의 핵심은 로그를 어떻게 남기느냐, 드리프트 감지를 어떻게 하느냐보다 파이프라인이 중요하다고 생각하고 작성했습니다.&lt;br /&gt;작성하면서 BentoML의 로깅에 대해 찾아보고 Fluent Bit에 대해서도 알게 되었는데 너무 가볍고 좋은 도구라고 생각들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 단순히 로컬에서 수행해봤지만, 충분히 확장성 있는 환경에서도 수행할 수 있게 구현 가능하다고 생각됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 생략한 코드가 있는데 사용한 코드는 모두 &lt;a href=&quot;https://github.com/Ssuwani/data-drift-detection&quot;&gt;https://github.com/Ssuwani/data-drift-detection&lt;/a&gt; 에 코드를 올려두었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Refences&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.bentoml.org/en/latest/guides/observability/monitoring-and-data-collection.html&quot;&gt;https://docs.bentoml.org/en/latest/guides/observability/monitoring-and-data-collection.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.fluentbit.io/manual/pipeline/inputs&quot;&gt;https://docs.fluentbit.io/manual/pipeline/inputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.evidentlyai.com/presets/data-drift&quot;&gt;https://docs.evidentlyai.com/presets/data-drift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ChatGPT&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>MLOps</category>
      <category>BentoML</category>
      <category>datadrift</category>
      <category>evidently ai</category>
      <category>fluentbit</category>
      <category>MLOps</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/185</guid>
      <comments>https://suwani.tistory.com/185#entry185comment</comments>
      <pubDate>Mon, 21 Oct 2024 22:26:16 +0900</pubDate>
    </item>
    <item>
      <title>DynamoDB와 Read / Write Capacity</title>
      <link>https://suwani.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DynamoDB는 AWS에서 제공하는 관리형 NoSQL 데이터베이스 서비스입니다. NoSQL이 갖는 장점과 쉬운 사용성, 그리고 빠른 성능과 자동 확장 기능을 갖는데요.&lt;br /&gt;DynamoDB의 장/단점에 대해 이해하고 제대로 사용하기 위해 꼭 필요하다고 생각하는 DynamoDB의 Read / Write Capacity에 대해 알아보고 제 생각을 정리해 봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아젠다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DynamoDB의 장점 / 단점&lt;/li&gt;
&lt;li&gt;Read / Write Capacity 조절의 필요성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. DynamoDB의 장점 / 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 사용하면서 느꼈던 장점과 단점을 몇 가지 짚어볼게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 모델링: 데이터 모델링이 유연합니다. 컬럼을 자유롭게 추가/삭제할 수 있고 데이터 타입도 다양하게 사용할 수 있어요.(JSON)&lt;/li&gt;
&lt;li&gt;완전 관리형 서비스: 서버 관리에 대해 신경 쓰지 않아도 돼서 편리합니다.&lt;/li&gt;
&lt;li&gt;자동 확장성: 애플리케이션이 성장함에 따라, 읽기/쓰기 요청이 폭발적으로 증가할 때도 자동으로 확장되니까 트래픽 변화에 유연하게 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용: 사용량이 적을 때는 저렴하지만, 대규모로 사용하게 되면 비용이 빠르게 증가할 수 있습니다. &lt;b&gt;특히 Read/Write Capacity를 제대로 관리하지 않으면 불필요한 비용이 발생할 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;트랜잭션 지원 제한: DynamoDB는 기본적으로 단순 조회나 삽입, 삭제 작업에 최적화되어 있지만, 복잡한 트랜잭션을 처리하는 데는 다소 한계가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Read / Write Capacity 조절의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DynamoDB를 사용하면서 가장 중요한 부분 중 하나가 바로 Read / Write Capacity 조절입니다. 비용 및 성능과 직결되어 있기 때문인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DynamoDB는 크게 두 가지 Capacity 모드로 작동합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로비저닝 모드: 필요한 용량을 미리 설정&lt;/li&gt;
&lt;li&gt;온디맨드 모드: 사용량에 따라 자동 확장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용이 문제가 되지 않는다면 너무 쉽게 온디맨드를 선택해 사용할 수 있을 것입니다.&lt;br /&gt;하지만 두 모드에 대해 가격을 계산해 보니 동일 요청에 대해 온디맨드가 7.5배 정도 비싸다는 것을 확인했습니다. (&lt;a href=&quot;https://aws.amazon.com/ko/dynamodb/pricing&quot;&gt;Dynamodb prices&lt;/a&gt;) - 쓰기 100만 건, 읽기 100만건 가정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 프로비저닝 모드를 사용해야 합니다;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다행인 점은 프로비저닝 모드에서 Auto Scaling을 지원한다는 점입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5ueJV/btsJ9uCsViz/IMEHQ0Q5QkC0arrkPEG7Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5ueJV/btsJ9uCsViz/IMEHQ0Q5QkC0arrkPEG7Dk/img.png&quot; data-alt=&quot;DynamoDB의 테이블 생성 시 Read / Write Capacity 조정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5ueJV/btsJ9uCsViz/IMEHQ0Q5QkC0arrkPEG7Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5ueJV%2FbtsJ9uCsViz%2FIMEHQ0Q5QkC0arrkPEG7Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;558&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DynamoDB의 테이블 생성 시 Read / Write Capacity 조정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read / Write Capacity를 정확히 산정하지 못하더라도 &lt;b&gt;Auto Scaling 옵션을 사용해 자동으로 조정&lt;/b&gt;할 수 있습니다.&lt;br /&gt;하지만 이 옵션만을 믿고 DynamoDB를 사용하다 보면 예상치 못한 비용이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 많은 요청을 DynamoDB가 처리한다면 많은 RCU (Read Capacity Units) / WCU (Write Capacity Units)를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DynamoDB에 요청을 추가할 때 항상 이 부분을 염두에 둬야 한다고 생각합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;극단적 예시로 오히려 Read/Write Capacity를 고정해 두고 사용한다면 무분별한 사용으로 많은 부하가 들어올 때 Latency가 증가하고 이때 Read / Write Capacity가 조절할 수 있을 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 생각을 정리하자면 초기에 Read/Write Capacity를 정확하게 산정할 수 없을 때 프로비저닝 모드의 Auto Scaling을 적극적으로(최소 / 최대 크게) 사용하되, 서비스가 안정화되면 Auto Scaling를 소극적으로(최대 작게) 사용하는 게 좋다고 생각합니다. (Auto Scaling은 적게 사용할 때 Scale in 함으로 써 비용을 절감할 수 있기도 함)&lt;/p&gt;</description>
      <category>데이터 엔지니어링</category>
      <category>dynamoDB</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/184</guid>
      <comments>https://suwani.tistory.com/184#entry184comment</comments>
      <pubDate>Thu, 17 Oct 2024 14:49:49 +0900</pubDate>
    </item>
    <item>
      <title>BentoML로 패키징하여 배포하고 Prometheus로 모니터링</title>
      <link>https://suwani.tistory.com/183</link>
      <description>&lt;p&gt;&lt;em&gt;블로그에서 필요한 코드들은 &lt;a href=&quot;https://github.com/Ssuwani/bentoml_prometheus&quot;&gt;https://github.com/Ssuwani/bentoml_prometheus&lt;/a&gt; 에 정리해두었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;0. 들어가며&lt;/h2&gt;
&lt;p&gt;BentoML은 모델을 쉽게 패키징하고 배포할 수 있는 오픈소스 라이브러리입니다. 또한, 배포된 모델에 대한 온라인 모니터링을 지원합니다.&lt;/p&gt;
&lt;p&gt;이 글에서 수행할 과정을 간단히 그림으로 나타내면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO8EzJ/btsJZJ16Wgb/7ZK8viNDzXH8BYnm8CdAOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO8EzJ/btsJZJ16Wgb/7ZK8viNDzXH8BYnm8CdAOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO8EzJ/btsJZJ16Wgb/7ZK8viNDzXH8BYnm8CdAOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO8EzJ%2FbtsJZJ16Wgb%2F7ZK8viNDzXH8BYnm8CdAOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1566&quot; height=&quot;417&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PyTorch로 모델 학습하고 평가하기&lt;/li&gt;
&lt;li&gt;BentoML로 모델 패키징하여 Containerize 한 뒤 배포하기&lt;/li&gt;
&lt;li&gt;Prometheus로 모델 모니터링하기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. PyTorch로 Fashion MNIST 모델을 학습하고 평가하기&lt;/h2&gt;
&lt;p&gt;이 글에서 데이터 &amp;amp; 모델은 중요한 부분은 아니라 &lt;a href=&quot;https://tutorials.pytorch.kr/beginner/basics/quickstart_tutorial.html&quot;&gt;PyTorch 공식 문서의 Quickstart 예제&lt;/a&gt;를 따라 데이터를 가져오고 모델을 학습한 뒤 평가해보겠습니다.&lt;/p&gt;
&lt;h3&gt;1.1. DataLoader 준비&lt;/h3&gt;
&lt;p&gt;여기서 DataLoader는 Iterable 한 객체를 의미합니다.&lt;/p&gt;
&lt;p&gt;정의한 뒤 대충 아래와 같이 사용됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for x, y in dataloader:
    pred = model(x) # 순전파
    print(f&amp;quot;y_pred: {pred}&amp;quot;)
    print(f&amp;quot;y_true: {y}&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 DataLoader를 얻기 위해선 먼저 Dataset을 가져와야 합니다.&lt;/p&gt;
&lt;p&gt;이제 Fashion MNIST Dataset을 가져와보겠습니다.&lt;br&gt;(&lt;em&gt;Fashion MNIST: 의류 분류 데이터 셋으로 10개의 카테고리로 분류&lt;/em&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from torchvision import datasets
from torchvision.transforms import ToTensor

training_data = datasets.FashionMNIST(
    root=&amp;quot;data&amp;quot;,
    train=True,
    download=True,
    transform=ToTensor(),
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;torchvision의 datasets로부터 fashionMNIST 데이터셋을 가져왔는데 더 많은 목록은 &lt;a href=&quot;https://pytorch.org/vision/stable/datasets.html&quot;&gt;여기&lt;/a&gt;서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;사용된 생성자 매개변수에 대해 간단히만 설명하면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root: 데이터를 다운받을 경로&lt;/li&gt;
&lt;li&gt;train: 학습 데이터 여부&lt;/li&gt;
&lt;li&gt;download: 데이터가 없을 경우 다운로드 여부&lt;/li&gt;
&lt;li&gt;transform: 데이터 변환 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저는 아래와 같이 데이터셋이 준비되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data/
└── FashionMNIST
    └── raw
        ├── t10k-images-idx3-ubyte
        ├── t10k-images-idx3-ubyte.gz
        ├── t10k-labels-idx1-ubyte
        ├── t10k-labels-idx1-ubyte.gz
        ├── train-images-idx3-ubyte
        ├── train-images-idx3-ubyte.gz
        ├── train-labels-idx1-ubyte
        └── train-labels-idx1-ubyte.gz&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지금까지 Dataset을 다운받았습니다. 이로부터 DataLoader를 정의해야 합니다. 간단히 정의할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from torch.utils.data import DataLoader

batch_size = 64
train_dataloader = DataLoader(training_data, batch_size=batch_size)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DataLoader는 Iterable 한 객체라고 했습니다. batch_size를 지정하지 않으면 1개씩 데이터를 가져오는데 이는 모델 학습에 매우 비효율적입니다. 적절히 메모리 크기를 고려해서 batch_size를 정해야 할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;1.2. 모델 정의 및 학습&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;모델 정의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PyTorch로 모델을 만들기 위해선 nn.Module을 상속받은 클래스를 정의하고 layer를 정의한 뒤 forward 함수에서 순전파를 정의하면 됩니다.&lt;/p&gt;
&lt;p&gt;다운받은 각 이미지는 28*28의 크기를 갖고 있습니다. CNN 같은 기법을 사용하여 모델을 정의할 수 있지만 간단히 Flatten Layer를 사용해 1차원 텐서로 변환한 뒤 Linear Layer를 사용해 분류 문제를 해결하고자 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from torch import nn

class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512), # In: 28*28, Out: 512 (28*28의 이미지가 Flatten 되어 512개의 텐서가 입력)
            nn.ReLU(),
            nn.Linear(512, 512), # In: 512, Out: 512
            nn.ReLU(),
            nn.Linear(512, 10), # In: 512, Out: 10 -&amp;gt; 10개의 카테고리로 분류
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x) # 10개의 텐서 = 각 클래스에 대한 확률
        return logits

model = NeuralNetwork()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;모델 학습&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모델을 학습한다는 건 각 레이어의 매개변수들을 조정하는 과정입니다. 순전파의 결과와 실제 결과로부터 Loss를 구하고 이를 최소화하는 방향으로 매개변수를 조정하는 역전파 과정을 통해 매개변수가 업데이트 됩니다.&lt;/p&gt;
&lt;p&gt;이를 위해선 Loss를 구하기 위한 loss_fn, 매개변수를 조정하기 위한 optimizer가 필요합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;분류 문제에서 직관적이고 쉽게 Loss를 구할 수 있는 CrossEntropyLoss를 사용하였습니다. 또한 매개변수를 조정하기 위한 Optimizer는 Gradient Descent 기법 중 하나 인 SGD를 사용하였습니다.&lt;/p&gt;
&lt;p&gt;학습은 아래와 같은 단계로 수행됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;epochs = 3
for epoch in range(epochs):
    print(f&amp;quot;Epoch {epoch+1}\n-------------------------------&amp;quot;)
    for batch, (x, y) in enumerate(train_dataloader):
        pred = model(x) # 순전파
        loss = loss_fn(pred, y) # Loss 계산
        loss.backward() # 역전파
        optimizer.step() # 매개변수 업데이트
        optimizer.zero_grad() # 그레이디언트 초기화
        if batch % 100 == 0: # 100번째 배치마다 Loss 출력
            print(f&amp;quot;loss: {loss.item():&amp;gt;7f}&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3. 모델 평가&lt;/h3&gt;
&lt;p&gt;모델 평가는 모델이 지금까지 보지 못한 데이터로 수행되어야 합니다. 위 Test Dataset를 가져오는 부분은 생략되었지만 간단히 작성해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 1. Test Dataset 가져오기
test_data = datasets.FashionMNIST(
    root=&amp;quot;data&amp;quot;,
    train=False,
    download=True,
    transform=ToTensor(),
)

# 2. Test DatasetLoader 정의
test_dataloader = DataLoader(test_data, batch_size=64)

# 3. 모델 평가
model.eval() # 모델을 평가 모드로 설정
correct = 0
with torch.no_grad():
    for x, y in test_dataloader:
        pred = model(x)
        correct += (pred.argmax(1) == y).type(torch.float).sum().item()

correct /= len(test_dataloader.dataset)
print(f&amp;quot;Accuracy: {(100*correct):&amp;gt;0.1f}%&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4. 전체 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Ssuwani/bentoml_prometheus/blob/main/train.py&quot;&gt;https://github.com/Ssuwani/bentoml_prometheus/blob/main/train.py&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ./train.py
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

training_data = datasets.FashionMNIST(
    root=&amp;quot;data&amp;quot;,
    train=True,
    download=True,
    transform=ToTensor(),
)

test_data = datasets.FashionMNIST(
    root=&amp;quot;data&amp;quot;,
    train=False,
    download=True,
    transform=ToTensor(),
)

batch_size = 64
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)


class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(
                28 * 28, 512
            ),  # In: 28*28, Out: 512 (28*28의 이미지가 Flatten 되어 512개의 텐서가 입력)
            nn.ReLU(),
            nn.Linear(512, 512),  # In: 512, Out: 512
            nn.ReLU(),
            nn.Linear(512, 10),  # In: 512, Out: 10 -&amp;gt; 10개의 카테고리로 분류
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)  # 10개의 텐서 = 각 클래스에 대한 확률
        return logits


model = NeuralNetwork()

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)

epochs = 3
for epoch in range(epochs):
    print(f&amp;quot;Epoch {epoch+1}\n-------------------------------&amp;quot;)
    for batch, (x, y) in enumerate(train_dataloader):
        pred = model(x)  # 순전파
        loss = loss_fn(pred, y)  # Loss 계산
        loss.backward()  # 역전파
        optimizer.step()  # 매개변수 업데이트
        optimizer.zero_grad()  # 그레이디언트 초기화
        if batch % 100 == 0:  # 100번째 배치마다 Loss 출력
            print(f&amp;quot;loss: {loss.item():&amp;gt;7f}&amp;quot;)

model.eval()  # 모델을 평가 모드로 설정
correct = 0
with torch.no_grad():
    for x, y in test_dataloader:
        pred = model(x)
        correct += (pred.argmax(1) == y).type(torch.float).sum().item()

correct /= len(test_dataloader.dataset)
print(f&amp;quot;Accuracy: {(100*correct):&amp;gt;0.1f}%&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. BentoML로 모델 패키징하여 Containerize 한 뒤 배포하기&lt;/h2&gt;
&lt;p&gt;BentoML은 모델 추론에 최적화된 서빙 시스템을 구축하기 위한 라이브러리입니다. Scikit-learn, PyTorch, Tensorflow 등 다양한 모델을 지원합니다.&lt;/p&gt;
&lt;p&gt;모델을 쉽게 저장하고 패키징할 수 있으며 배포할 수 있게 도와줍니다.&lt;/p&gt;
&lt;h3&gt;2.1. BentoML로 모델 저장하기&lt;/h3&gt;
&lt;p&gt;학습된 모델을 BentoML로 쉽게 저장할 수 있습니다. 이때 아래 코드와 같이 모델의 레이블과 메타데이터를 추가할 수 있습니다. 그리고 필요하다면 Custom Object를 추가할 수 있습니다. (ex, labels, tokenizer 등)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import bentoml

bentoml.pytorch.save_model(
    &amp;quot;fashion_mnist_model&amp;quot;,
    model,
    labels={&amp;quot;framework&amp;quot;: &amp;quot;pytorch&amp;quot;},
    metadata={
        &amp;quot;accuracy&amp;quot;: correct,
        &amp;quot;author&amp;quot;: &amp;quot;jangsuwan&amp;quot;,
    },
    custom_objects={
        &amp;quot;labels&amp;quot;: [&amp;quot;T-shirt&amp;quot;, &amp;quot;Trouser&amp;quot;, &amp;quot;Pullover&amp;quot;, &amp;quot;Dress&amp;quot;, &amp;quot;Coat&amp;quot;, &amp;quot;Sandal&amp;quot;, &amp;quot;Shirt&amp;quot;, &amp;quot;Sneaker&amp;quot;, &amp;quot;Bag&amp;quot;, &amp;quot;Ankle boot&amp;quot;],
    },
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 Bento Model이 생성되었습니다. 이는 CLI를 통해서도 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bentoml models list

#  Tag                                   Module           Size      Creation Time
#  fashion_mnist_model:2ldmcvug2gl5ssxv  bentoml.pytorch  2.56 MiB  2024-10-10 15:35:21&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 저장된 모델은 현재 서버에 등록되어 있습니다. 필요 시 S3와 같은 저장소에 저장할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;!pip install bentoml[aws]
bentoml.export_bento(&amp;#39;fashion_mnist_model:latest&amp;#39;, &amp;#39;s3://my_bucket/folder&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 저장된 S3로부터 Bento Model을 불러올 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;bentoml.import_bento(&amp;#39;s3://my_bucket/folder&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2. BentoML로 모델 패키징하기&lt;/h3&gt;
&lt;p&gt;앞서 저장한 Bento Model을 통해 API를 생성할 수 있도록 Service 코드를 생성해보겠습니다.&lt;/p&gt;
&lt;p&gt;간단히 먼저 소개해보면 Bento Model로 부터 bentoml의 runner를 생성할 수 있습니다. 이는 모델을 추론하기 위한 인터페이스를 제공합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import bentoml
import torch
runner = bentoml.pytorch.get(&amp;quot;fashion_mnist_model:latest&amp;quot;).to_runner()

runner.init_local()
runner.run(torch.randn(1, 28, 28))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runner가 추론을 할 수 있는 인스턴스임을 알았습니다. 이제 이 Runner를 통해 API를 생성해보겠습니다.&lt;/p&gt;
&lt;p&gt;service.py 파일을 생성하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Ssuwani/bentoml_prometheus/blob/main/service.py&quot;&gt;https://github.com/Ssuwani/bentoml_prometheus/blob/main/service.py&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ./service.py
import bentoml
from bentoml.io import Image as PILImage
import numpy as np

bento_model = bentoml.pytorch.get(&amp;quot;fashion_mnist_model:latest&amp;quot;)
runner = bento_model.to_runner()
labels = bento_model.custom_objects[&amp;quot;labels&amp;quot;]

svc = bentoml.Service(
    name=&amp;quot;fashion_mnist_model&amp;quot;,
    runners=[runner],
)

@svc.api(input=bentoml.io.Image(), output=bentoml.io.JSON())
async def predict(image: PILImage):
    arr = np.array(image)
    assert arr.shape == (28, 28)

    arr = np.expand_dims(arr, 0).astype(&amp;quot;float32&amp;quot;)  # 차원 추가
    output_tensor = await runner.async_run(arr)
    prediction = output_tensor.numpy().argmax()

    return {&amp;quot;label&amp;quot;: labels[prediction], &amp;quot;idx&amp;quot;: prediction}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(28, 28) 크기의 하나의 이미지를 입력받아 추론하는 코드를 작성하였습니다. 모델의 입력은 [batch_size, height, width]의 크기를 갖기에 차원을 추가해주는 코드도 추가하였습니다.&lt;/p&gt;
&lt;p&gt;이제 로컬에서 서버를 실행할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bentoml serve service.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt; 에 접속하여 모델이 정상적으로 동작하는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3136&quot; data-origin-height=&quot;2306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/of4Sb/btsJ16OqnTH/gnjCr64mMcrzzu55rzkX3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/of4Sb/btsJ16OqnTH/gnjCr64mMcrzzu55rzkX3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/of4Sb/btsJ16OqnTH/gnjCr64mMcrzzu55rzkX3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fof4Sb%2FbtsJ16OqnTH%2FgnjCr64mMcrzzu55rzkX3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3136&quot; height=&quot;2306&quot; data-origin-width=&quot;3136&quot; data-origin-height=&quot;2306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;2.3. BentoML로 모델 배포하기 (Containerize)&lt;/h3&gt;
&lt;p&gt;로컬에서 모델 엔드포인트 생성까지 잘 되었고 테스트해보았습니다. 그런데 저희의 목표는 로컬에서 해당 서비스를 실생하는 게 아니라 컨테이너화하여 배포하는 것입니다.&lt;/p&gt;
&lt;p&gt;이를 위해선 Bento 라는 형태로 패키징해야 합니다. 이를 위해선 bentofile.yaml 파일을 생성하고 필요한 항목들을 작성해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Ssuwani/bentoml_prometheus/blob/main/bentofile.yaml&quot;&gt;https://github.com/Ssuwani/bentoml_prometheus/blob/main/bentofile.yaml&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# bentofile.yaml
service: &amp;quot;service:svc&amp;quot;

labels:
  owner: jangsuwan
  framework: pytorch

include:
  - &amp;quot;*.py&amp;quot;

python:
  packages:
    - torch
    - numpy
    - pillow

models:
  - fashion_mnist_model:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 컨테이너화하여 배포할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bentoml build
bentoml containerize --platform linux/amd64 fashion_mnist_model --image-tag fashion_mnist_model:latest
docker run --rm -p 3000:3000 fashion_mnist_model:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지금까지 PyTorch 모델을 학습하고 BentoML Model로 저장하고 패키징한 뒤 컨테이너화하였고 이를 docker run 명령어를 통해 배포하였습니다.&lt;/p&gt;
&lt;h2&gt;3. Prometheus로 모델 모니터링하기&lt;/h2&gt;
&lt;p&gt;BentoML은 모델 성능을 쉽게 모니터링 할 수 있도록 기본적으로 메트릭 수집 기능을 제공합니다.&lt;br&gt;&lt;a href=&quot;http://localhost:3000/metrics&quot;&gt;http://localhost:3000/metrics&lt;/a&gt; 에서 메트릭을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;3.1. BentoML의 메트릭 수집 확인&lt;/h3&gt;
&lt;p&gt;BentoML은 기본적으로 아래 메트릭이 정의되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 서버 진행 중 요청 수&lt;/li&gt;
&lt;li&gt;Runner 진행중 요청 수&lt;/li&gt;
&lt;li&gt;API 서버 총 요청&lt;/li&gt;
&lt;li&gt;Runner 총 요청&lt;/li&gt;
&lt;li&gt;API 서버 요청 처리 시간&lt;/li&gt;
&lt;li&gt;Runner 요청 처리 시간&lt;/li&gt;
&lt;li&gt;Runner에 들어오는 배치 사이즈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또한, Custom Metric을 Service에 정의할 수도 있습니다.&lt;/p&gt;
&lt;h3&gt;3.2. Prometheus &amp;amp; Grafana 설치&lt;/h3&gt;
&lt;p&gt;Prometheus는 메트릭을 수집하고 저장하는 역할을 합니다. Grafana는 메트릭을 시각화하는 역할을 합니다.&lt;/p&gt;
&lt;p&gt;docker compose를 통해 설치해보겠습니다.&lt;/p&gt;
&lt;p&gt;docker-compose.yml 파일은 &lt;a href=&quot;https://github.com/Ssuwani/bentoml_prometheus/blob/main/monitoring/docker-compose.yml&quot;&gt;여기&lt;/a&gt;에서 확인할 수 있고 중요한 부분은 prometheus/prometheus.yml 파일에 정의된 BentoML Metrics를 Scrape 하는 부분입니다.&lt;/p&gt;
&lt;p&gt;모든 리소스가 로컬의 Docker에 의해 실행되고 있으므로 host는 docker 에서 제공하는 dns로 설정했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;scrape_configs:
  - job_name: &amp;#39;bentoml&amp;#39;
    static_configs:
      - targets: [&amp;#39;host.docker.internal:3000&amp;#39;]
    metrics_path: &amp;#39;/metrics&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 아래 명령어룰 수행하면 Prometheus와 Grafana가 실행됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# git clone https://github.com/Ssuwani/bentoml_prometheus
cd monitoring
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Prometheus: &lt;a href=&quot;http://localhost:9090&quot;&gt;http://localhost:9090&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Grafana: &lt;a href=&quot;http://localhost:3001&quot;&gt;http://localhost:3001&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3. 대시보드 생성하여 모니터링&lt;/h3&gt;
&lt;p&gt;마지막으로 Prometheus와 Grafana에서 BentoML Metric이 정상 집계되는지 확인합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prometheus에서 Status -&amp;gt; Service Discovery에서 BentoML 확인&lt;/li&gt;
&lt;li&gt;Grafana 로그인 (admin/admin)&lt;/li&gt;
&lt;li&gt;Grafana의 Explore 혹은 대시보드 생성하여 &lt;code&gt;bentoml_&lt;/code&gt; 메트릭 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3532&quot; data-origin-height=&quot;2158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PTp8e/btsJ0bqkxrg/OpaR1J9CR8d4bBXlY2pX60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PTp8e/btsJ0bqkxrg/OpaR1J9CR8d4bBXlY2pX60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PTp8e/btsJ0bqkxrg/OpaR1J9CR8d4bBXlY2pX60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPTp8e%2FbtsJ0bqkxrg%2FOpaR1J9CR8d4bBXlY2pX60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3532&quot; height=&quot;2158&quot; data-origin-width=&quot;3532&quot; data-origin-height=&quot;2158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>MLOps</category>
      <category>BentoML</category>
      <category>MLOps</category>
      <category>Prometheus</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/183</guid>
      <comments>https://suwani.tistory.com/183#entry183comment</comments>
      <pubDate>Thu, 10 Oct 2024 17:55:04 +0900</pubDate>
    </item>
    <item>
      <title>요기요에서의 2년 5개월 회고</title>
      <link>https://suwani.tistory.com/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 2022.05.02 ~ 2024.09.30, 약 2년 5개월간 요기요에서 근무했습니다. 기억하고 싶은 내용을 위해 글을 작성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운이 좋았던 입사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 곳에서 서류 탈락했습니다. 놀라운 것은 요기요도 포함이었습니다. 그런데, 일주일 뒤 인사팀으로부터 전화가 왔고 면접 일정을 잡게 되었습니다. 나중에 들어보니, 리더분이 제 이력을 보고 면접을 보고 싶어 했다고 전해 들었습니다. 다시 한번 감사드립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마음이 힘들었던 입사 후&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022.05.02&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 건물은 생각보다 더 컸고, &quot;이런 곳에 내가 다니다니!&quot;라며 한참 어깨가 으쓱거렸습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Njzoq/btsJRpoCcHw/vquJkKR4mq9kkESDYMnSoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Njzoq/btsJRpoCcHw/vquJkKR4mq9kkESDYMnSoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Njzoq/btsJRpoCcHw/vquJkKR4mq9kkESDYMnSoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNjzoq%2FbtsJRpoCcHw%2FvquJkKR4mq9kkESDYMnSoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;214&quot; height=&quot;285&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 사원증을 목에 걸고 다니면 기분이 좋았습니다. (입사 초기 한정..ㅋㅋ)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FQwxj/btsJSnch1RL/9wn7hyccaGJHYpB7iNxqvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FQwxj/btsJSnch1RL/9wn7hyccaGJHYpB7iNxqvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FQwxj/btsJSnch1RL/9wn7hyccaGJHYpB7iNxqvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFQwxj%2FbtsJSnch1RL%2F9wn7hyccaGJHYpB7iNxqvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;191&quot; height=&quot;255&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분들께 인사하고 본격적으로 온보딩을 시작했습니다. 기대했던 것과 다르게 온보딩 과정은 저에게 힘든 시간이었습니다. 다들 많이 바빴고 정신이 없었습니다. 그런데 데일리 스탠드업에서 며칠째 제가 하는 말은 동일했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;어제 온보딩 문서 봤고, 오늘도 온보딩 문서 볼 예정입니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 조급했습니다. 빨리 나를 증명해야 한다고 생각했습니다. 이런 저를 많은 분들이 다독여 주셨지만, 마음의 불편함은 몇 주간 이어졌습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 조직에서의 ML 서비스 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022.05 ~ 2023.02&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 후 운이 좋게도 원하던 MLOps 업무를 할 수 있었습니다. 다음과 같은 업무들을 수행했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전달받은 쿼리로 Data Pipeline 개발 및 운영&lt;/li&gt;
&lt;li&gt;전달받은 모델 학습 코드로 ML Pipeline 개발 및 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 모델을 맡았고, 팀원들은 제가 맡은 모델에 관해 제 의견을 많이 존중해 주셨습니다. 좋은 사람들과 웃으며 일할 수 있었고, 한강 나들이 &amp;amp; 강릉 워케이션 등 좋은 추억도 정말 많이 쌓았습니다. 또한, R&amp;amp;D 구성원을 대상으로 구성한 MLOps 아키텍처를 소개할 기회도 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3444&quot; data-origin-height=&quot;1432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfm1qp/btsJQ4SAokx/sjTtdsEzKP1gnFRTkvnSJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfm1qp/btsJQ4SAokx/sjTtdsEzKP1gnFRTkvnSJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfm1qp/btsJQ4SAokx/sjTtdsEzKP1gnFRTkvnSJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfm1qp%2FbtsJQ4SAokx%2FsjTtdsEzKP1gnFRTkvnSJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;311&quot; data-origin-width=&quot;3444&quot; data-origin-height=&quot;1432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목적 조직으로의 이동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023.02 ~ 2024.04&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 데이터 조직에서 개발했던 내용을 가지고 목적 조직으로 이동(전배)했습니다.&lt;br /&gt;데이터 조직에 있으면서의 문제점은 데이터 사이언티스트(DS) 분들과의 협업이었습니다. DS 분들은 이미 목적 조직에 있었고, 서로 다른 조직에 있다 보니 커뮤니케이션이 원활하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때부터 기존 팀에서의 BE/FE 분들과 멀어지면서 업무가 추가되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ML 서비스를 위한 Backend 개발 및 운영&lt;/li&gt;
&lt;li&gt;ML 서비스를 위한 Frontend 개발 및 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배달 시간 예측을 위한 기존 Backend는 복수개의 MSA로 구성되어 있었습니다. (Geo, Vendor 등 서로 다른 역할) 이를 유지 및 관리 효율화를 위해 하나의 MS로 통합하였습니다. 또한, 기존에 React로 작성되었던 Frontend 또한 유지 및 관리가 어려워 Streamlit으로 재개발하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 전사 비용 효율화를 위해 GCP에서 AWS로 이관하는 데 많은 시간을 사용했습니다. 이후 수많은 프로젝트에 참여하고 기여했습니다. 정말 많이 성장했던 시기였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다시 기능 조직으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024.05 ~ 2024.09&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요기요에는 크게 3가지 ML 프러덕이 존재했습니다. (추천 시스템, 이미지 &amp;amp; 텍스트, 배달 시간 예측)&lt;br /&gt;각 프러덕은 서로 다른 팀이 관리했고, 서로 다른 시스템을 사용했습니다. 잘하는 사람들이 여기저기 흩어져 있었고, 함께 한다면 시너지가 날 것이라는 의견이 많아 팀이 합쳐졌습니다. MLOps 팀이 생겼고, 이때부터 추천 시스템을 온보딩하며 아주 간단한 추천인 '주문내역 스윔레인'을 개발하고 배포했습니다.&lt;br /&gt;같은 일을 하는 사람들과 함께 팀이 되어 많은 것을 배웠고, 앞으로 계획된 일들도 기대했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;떠나며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024.09 회사는 희망 퇴직을 신청 받았고, 정말 많은 고민을 했습니다. 고민을 한 이유는 두 가지였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 &lt;b&gt;사람&lt;/b&gt;들과 아직도 더 일하고 싶다는 생각&lt;/li&gt;
&lt;li&gt;다시 취업할 수 있을까 하는 &lt;b&gt;자신감&lt;/b&gt; 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 고민은 이력서를 작성해보며 어느 정도 해소되었습니다. 많은 일을 했고, 제가 한 일에 대해서는 자신 있게 말할 수 있었습니다. 첫 번째 고민은 너무 불안정한 고민이라는 생각에 퇴사를 결정했습니다. IT 업계에서 좋은 사람을 만나 함께 일할 수 있는 건 축복이지만, 누구든 떠날 수 있기에 너무 의존해서도 안 된다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 첫날 먹었던 다미선을 마지막 날에도 찾았고, 집으로 돌아왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca9A9V/btsJQH4mFkL/CqAkpcokk3z5paytDRksR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca9A9V/btsJQH4mFkL/CqAkpcokk3z5paytDRksR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca9A9V/btsJQH4mFkL/CqAkpcokk3z5paytDRksR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca9A9V%2FbtsJQH4mFkL%2FCqAkpcokk3z5paytDRksR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;275&quot; height=&quot;367&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕, 요기요 ㅠㅠㅠ&lt;/p&gt;</description>
      <category>요기요</category>
      <category>퇴사</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/182</guid>
      <comments>https://suwani.tistory.com/182#entry182comment</comments>
      <pubDate>Mon, 30 Sep 2024 21:56:43 +0900</pubDate>
    </item>
    <item>
      <title>RDD vs DataFrame in Spark</title>
      <link>https://suwani.tistory.com/181</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데이터 표현 방식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조화되지 않은 데이터도 처리할 수 있음&lt;/li&gt;
&lt;li&gt;사용자 정의 함수를 통해 데이터 변환하는 데 유연&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DataFrame&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDD 위에 구조화된 데이터 추가한 것&lt;/li&gt;
&lt;li&gt;행과 열로 이루어져 있으며 Pandas의 Dataframe과 유사&lt;/li&gt;
&lt;li&gt;명확한 스키마를 가지고 있기에 데이터 처리 및 변환이 직관적이고 효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;API 수준&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낮은 수준의 API 제공&lt;/li&gt;
&lt;li&gt;사용자에게 더 많은 제어권 제공&lt;/li&gt;
&lt;li&gt;map, reduce, filter 등의 함수형 프로그래밍 연산자 이용해 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DataFrame&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 수준의 API 제공&lt;/li&gt;
&lt;li&gt;SQL 쿼리와 유사한 방식으로 데이터 처리 -&amp;gt; 직관적&lt;/li&gt;
&lt;li&gt;최적화된 실행 계획을 통해 효율적으로 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 정의함수를 사용하기에 최적화 여지가 적음&lt;/li&gt;
&lt;li&gt;실행 계획 최적화하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DataFrame&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Catalyst Optimizer라는 최적화 엔진을 사용해 실행계획 최적화&lt;/li&gt;
&lt;li&gt;-&amp;gt; RDD 보다 빠르고 효율적으로 데이터 처리 가능&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>데이터 엔지니어링</category>
      <category>DataFrame</category>
      <category>RDD</category>
      <category>Spark</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/181</guid>
      <comments>https://suwani.tistory.com/181#entry181comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:37:30 +0900</pubDate>
    </item>
    <item>
      <title>스파크 RDD 이해하기</title>
      <link>https://suwani.tistory.com/180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Apache spark에서는 데이터를 다루기 위해 아래 3가지 추상화 개념을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDD (Resilient Distributed Datasets)&lt;/li&gt;
&lt;li&gt;DataFrames&lt;/li&gt;
&lt;li&gt;Datasets&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 RDD에 대한 설명과 예제를 통해 이해해보고자 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDD는 Spark의 기본 데이터 구조로 병렬로 처리할 수 있는 불변 분산 객체 컬렉션&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 내에서 처리 연산을 지원함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 map reduce 파일 시스템과 비교했을 때의 장점&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;생성되면 수정할 수 없음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 RDD를 수정하는 게 아니라 새로운 RDD를 생성함. (불변성)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RDD는 장애로부터 쉽게 복구할 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 불변성 덕분에 RDD 복구가 쉽고 분산 환경에서 처리되기에 Node A에서 문제시 Node B에서 수행 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Laze Evaluation
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Action 연산자를 만나야 작업을 수행함 (Tranformation 연산은 아무리 쌓여도 실행하지 않음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴파일 시 타입 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 전 에러를 조기 발견할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformation 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;map, filter, distinct, union, intersection&lt;/li&gt;
&lt;li&gt;Return: RDD&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Action 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;collect, count, top(num), takeOrdered(num), reduce(func)&lt;/li&gt;
&lt;li&gt;Return: 실행 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 데이터로부터 RDD를 생성한 뒤 transformation 연산 &amp;amp; Action 연산을 수행해보고자 합니다.&lt;br /&gt;그리고 Spark WebUI를 통해 수행 작업을 시각화해서 확인해 봅니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pyspark import SparkContext, SparkConf


conf = SparkConf().setAppName(&quot;test_rdd&quot;).setMaster(&quot;local&quot;)
sc = SparkContext(conf=conf)

data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)

# transformation
added_distData = distData.map(lambda x: x+1)

# action
result = added_distData.top(3)

while True:
    pass # web ui 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://localhost:4040&quot;&gt;http://localhost:4040&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;에서 Stage 확인&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dt8IfI/btsJn0O1zEi/uxvhjEsq3OSzHz7kmyFhl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dt8IfI/btsJn0O1zEi/uxvhjEsq3OSzHz7kmyFhl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dt8IfI/btsJn0O1zEi/uxvhjEsq3OSzHz7kmyFhl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdt8IfI%2FbtsJn0O1zEi%2FuxvhjEsq3OSzHz7kmyFhl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2008&quot; height=&quot;1868&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>데이터 엔지니어링</category>
      <category>RDD</category>
      <category>Spark</category>
      <author>ssuwani</author>
      <guid isPermaLink="true">https://suwani.tistory.com/180</guid>
      <comments>https://suwani.tistory.com/180#entry180comment</comments>
      <pubDate>Sun, 1 Sep 2024 15:37:05 +0900</pubDate>
    </item>
  </channel>
</rss>