diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a768e2f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +# PR ์ œ๋ชฉ + +## ๐Ÿ“ ๊ฐœ์š” + + + +--- + +## โš™๏ธ ๊ตฌํ˜„ ๋‚ด์šฉ + + + +--- + +## ๐Ÿ“Ž ๊ธฐํƒ€ + + + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ + + + +--- \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..00aa09b --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,63 @@ +name: Deploy + +on: + push: + branches: ["develop"] + + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + # 1. checkout develop branch + - name: Checkout develop + uses: actions/checkout@v3 + with: + ref: develop + + # 2. Docker ๋กœ๊ทธ์ธ + - name: Login to Docker Hub + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + + # 3. ์ด๋ฏธ์ง€ build ๋ฐ push + - name: Build and Push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest + + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + + steps: + # 1. checkout branch + - name: Check PR + uses: actions/checkout@v3 + + # 2. EC2 pull + - name: EC2 Docker Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + port: ${{ secrets.SERVER_SSH_PORT }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_PRIVATE_KEY }} + script: | + cd ~/ai-server + + docker stop ai-server || true + docker rm ai-server || true + + docker system prune -a -f || true + + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker pull ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest + + docker run -d \ + --name ai-server \ + --env-file .env \ + -p 8000:8000 \ + ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0a19790..345ca2a 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,7 @@ cython_debug/ # PyPI configuration file .pypirc + +*quickdraw* + +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3ac4ea6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.10-slim AS builder + +WORKDIR /app + +COPY requirements.txt . + +RUN apt-get update && apt-get install -y libgl1 libglib2.0-0 +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + + + +FROM python:3.10-slim +WORKDIR /app +RUN apt-get update && apt-get install -y libgl1 libglib2.0-0 +COPY --from=builder /usr/local /usr/local + +COPY src/ src/ +COPY run.py config.py . + +CMD ["python", "run.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 820a69b..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 GotchaAI - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index e69de29..da5be35 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,40 @@ +## ๐Ÿ“Œ ์ปจ๋ฒค์…˜ + + +### ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ + +| message | description | +| --- | --- | +| feat | ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€, ๊ธฐ์กด ๊ธฐ๋Šฅ์„ ์š”๊ตฌ ์‚ฌํ•ญ์— ๋งž์ถ”์–ด ์ˆ˜์ • | +| fix | ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋ฒ„๊ทธ ์ˆ˜์ • | +| docs | ๋ฌธ์„œ(์ฃผ์„) ์ˆ˜์ • | +| style | ์ฝ”๋“œ ์Šคํƒ€์ผ, ํฌ๋งทํŒ…์— ๋Œ€ํ•œ ์ˆ˜์ • | +| refact | ๊ธฐ๋Šฅ ๋ณ€ํ™”๊ฐ€ ์•„๋‹Œ ์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง | +| test | ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€/์ˆ˜์ • | +| chore | ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ์ˆ˜์ •, ๊ทธ ์™ธ ๊ธฐํƒ€ ์ˆ˜์ • ex) .gitignore | + +## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +``` +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ main.py # entry point (FastAPI ๊ฐ์ฒด ์ƒ์„ฑ) +โ”‚ โ”œโ”€โ”€ api/ # ๋ผ์šฐํŒ… ๊ตฌ์„ฑ +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ image_routes.py # ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ด€๋ จ API ์—”๋“œํฌ์ธํŠธ +โ”‚ โ”‚ โ”œโ”€โ”€ myomyo_routes.py # MYOMYO ๋ฉ”์‹œ์ง€ ๊ด€๋ จ API ์—”๋“œํฌ์ธํŠธ +โ”‚ โ”‚ โ””โ”€โ”€ lulu_routes.py # LULU ๋ฉ”์‹œ์ง€ ๊ด€๋ จ API ์—”๋“œํฌ์ธํŠธ +โ”‚ โ”œโ”€โ”€ chat/ +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ myomyo.py # ๋ฌ˜๋ฌ˜ ํ”„๋กฌํ”„ํŠธ ๋ฐ ๊ฒŒ์ž„ ํ๋ฆ„ ๊ด€๋ฆฌ +โ”‚ โ”‚ โ””โ”€โ”€ lulu.py # ๋ฃจ๋ฃจ ํ”„๋กฌํ”„ํŠธ ๋ฐ ๊ฒŒ์ž„ ํ๋ฆ„ ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ image/ +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ trained_model/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ model.pth # quickdraw ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜ ๋ชจ๋ธ +โ”‚ โ”‚ โ”œโ”€โ”€ classifier.py # quickdraw ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜ ๊ธฐ๋Šฅ +โ”‚ โ”‚ โ”œโ”€โ”€ model.py # CNN ๋ชจ๋ธ ์ •์˜ +โ”‚ โ”‚ โ”œโ”€โ”€ img_caption.py # BLIP ๊ธฐ๋ฐ˜ captioning ๊ธฐ๋Šฅ +โ”‚ โ”‚ โ”œโ”€โ”€ preprocessor.py # ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ +โ”‚ โ”‚ โ””โ”€โ”€ text_masking.py # easyocr ๊ธฐ๋ฐ˜ ํ…์ŠคํŠธ ๋งˆ์Šคํ‚น ๊ธฐ๋Šฅ +``` diff --git a/app/config.py b/app/config.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 4c299ff..0000000 --- a/app/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import uvicorn -from fastapi import FastAPI - -app = FastAPI() diff --git a/app/routers/predict_router.py b/app/routers/predict_router.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/services/__init__.py b/app/services/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/app/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config.py b/config.py new file mode 100644 index 0000000..601b601 --- /dev/null +++ b/config.py @@ -0,0 +1,294 @@ + +TEXT_THRESHOLD=0.7 + +# QuickDraw ๋ฐ์ดํ„ฐ ์„ค์ • +IMAGE_SIZE = (32, 32) # ์ด๋ฏธ์ง€ ํฌ๊ธฐ (๋„ˆ๋น„, ๋†’์ด) +ENG_CATEGORIES= [ + "aircraft carrier", + "airplane", + "alarm clock", + "ambulance", + "angel", + "apple", + "arm", + "axe", + "backpack", + "banana", + "bandage", + "baseball", + "basketball", + "bat", + "bathtub", + "bed", + "bee", + "bicycle", + "bird", + "birthday cake", + "book", + "bowtie", + "bread", + "broom", + "bucket", + "bus", + "bush", + "butterfly", + "cake", + "calendar", + "camera", + "campfire", + "candle", + "car", + "carrot", + "cat", + "cell phone", + "chair", + "church", + "circle", + "cloud", + "compass", + "computer", + "cookie", + "couch", + "cow", + "crab", + "crocodile", + "crown", + "cup", + "dog", + "dolphin", + "donut", + "door", + "duck", + "ear", + "elephant", + "envelope", + "eye", + "eyeglasses", + "face", + "fan", + "fire hydrant", + "fish", + "flower", + "fork", + "frog", + "frying pan", + "garden", + "giraffe", + "grapes", + "guitar", + "hammer", + "hat", + "helicopter", + "hexagon", + "hockey stick", + "horse", + "ice cream", + "jacket", + "kangaroo", + "keyboard", + "knife", + "ladder", + "laptop", + "leaf", + "leg", + "lighthouse", + "lightning", + "lion", + "lobster", + "lollipop", + "mailbox", + "map", + "marker", + "megaphone", + "moon", + "motorbike", + "mountain", + "mug" +] +ENG_CATEGORIES= [ + "aircraft carrier", + "airplane", + "alarm clock", + "ambulance", + "angel", + "apple", + "arm", + "axe", + "backpack", + "banana", + "bandage", + "baseball", + "basketball", + "bat", + "bathtub", + "bed", + "bee", + "bicycle", + "bird", + "birthday cake", + "book", + "bowtie", + "bread", + "broom", + "bucket", + "bus", + "bush", + "butterfly", + "cake", + "calendar", + "camera", + "campfire", + "candle", + "car", + "carrot", + "cat", + "cell phone", + "chair", + "church", + "circle", + "cloud", + "compass", + "computer", + "cookie", + "couch", + "cow", + "crab", + "crocodile", + "crown", + "cup", + "dog", + "dolphin", + "donut", + "door", + "duck", + "ear", + "elephant", + "envelope", + "eye", + "eyeglasses", + "face", + "fan", + "fire hydrant", + "fish", + "flower", + "fork", + "frog", + "frying pan", + "garden", + "giraffe", + "grapes", + "guitar", + "hammer", + "hat", + "helicopter", + "hexagon", + "hockey stick", + "horse", + "ice cream", + "jacket", + "kangaroo", + "keyboard", + "knife", + "ladder", + "laptop", + "leaf", + "leg", + "lighthouse", + "lightning", + "lion", + "lobster", + "lollipop", + "mailbox", + "map", + "marker", + "megaphone", + "moon", + "motorbike", + "mountain", + "mug" +] + +KOR_CATEGORIES= [ + "ํ•ญ๊ณต๋ชจํ•จ", + "๋น„ํ–‰๊ธฐ", + "์•Œ๋žŒ์‹œ๊ณ„", + "์•ฐ๋ทธ๋Ÿฐ์Šค", + "์ฒœ์‚ฌ", + "์‚ฌ๊ณผ", + "ํŒ”", + "๋„๋ผ", + "๋ฐฑํŒฉ", + "๋ฐ”๋‚˜๋‚˜", + "๋ถ•๋Œ€", + "์•ผ๊ตฌ๊ณต", + "๋†๊ตฌ๊ณต", + "์•ผ๊ตฌ๋ฐฐํŠธ", + "์š•์กฐ", + "์นจ๋Œ€", + "๊ฟ€๋ฒŒ", + "์ž์ „๊ฑฐ", + "์ƒˆ", + "์ƒ์ผ์ผ€์ดํฌ", + "์ฑ…", + "๋‚˜๋น„๋„ฅํƒ€์ด", + "๋นต", + "๋น—์ž๋ฃจ", + "์–‘๋™์ด", + "๋ฒ„์Šค", + "์ˆ˜ํ’€", + "๋‚˜๋น„", + "์ผ€์ดํฌ", + "๋‹ฌ๋ ฅ", + "์นด๋ฉ”๋ผ", + "๋ชจ๋‹ฅ๋ถˆ", + "์–‘์ดˆ", + "์ฐจ", + "๋‹น๊ทผ", + "๊ณ ์–‘์ด", + "ํ•ธ๋“œํฐ", + "์˜์ž", + "๊ตํšŒ", + "๋™๊ทธ๋ผ๋ฏธ", + "๊ตฌ๋ฆ„", + "์ปดํŒŒ์Šค", + "์ปดํ“จํ„ฐ", + "์ฟ ํ‚ค", + "์†ŒํŒŒ", + "์†Œ", + "๊ฒŒ", + "์•…์–ด", + "์™•๊ด€", + "์ปต", + "๊ฐœ", + "๋Œ๊ณ ๋ž˜", + "๋„๋„›", + "๋ฌธ", + "์˜ค๋ฆฌ", + "๊ท€", + "์ฝ”๋ผ๋ฆฌ", + "ํŽธ์ง€๋ด‰ํˆฌ", + "๋ˆˆ", + "์•ˆ๊ฒฝ", + "์–ผ๊ตด", + "์„ ํ’๊ธฐ", + "์†Œํ™”๊ธฐ", + "๋ฌผ๊ณ ๊ธฐ", + "๊ฝƒ", + "ํฌํฌ", + "๊ฐœ๊ตฌ๋ฆฌ", + "ํ”„๋ผ์ดํŒฌ", + "์ •์›", + "๊ธฐ๋ฆฐ", + "ํฌ๋„", + "๊ธฐํƒ€", + "๋ง์น˜", + "๋ชจ์ž", + "ํ—ฌ๋ฆฌ์ฝฅํ„ฐ", + "์œก๊ฐํ˜•", + "ํ•˜ํ‚ค ์ฑ„", + "๋ง", + "์•„์ด์Šคํฌ๋ฆผ", + "์žฌํ‚ท", + "์บฅ๊ฑฐ๋ฃจ", "ํ‚ค๋ณด๋“œ", "์นผ", "์‚ฌ๋‹ค๋ฆฌ", "๋…ธํŠธ๋ถ", "๋‚˜๋ญ‡์žŽ", "๋‹ค๋ฆฌ", "๋“ฑ๋Œ€", "๋ฒˆ๊ฐœ", "์‚ฌ์ž", "๊ฐ€์žฌ", "๋ง‰๋Œ€์‚ฌํƒ•", "์šฐ์ฒดํ†ต", "์ง€๋„", "๋ณด๋“œ๋งˆ์นด", "ํ™•์„ฑ๊ธฐ", "๋‹ฌ", "์˜คํ† ๋ฐ”์ด", "์‚ฐ", "๋จธ๊ทธ์ปต"] + +MODEL_PATH= 'src/image/trained_model/' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..21a4f14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,11 @@ +matplotlib +numpy==1.26.4 +opencv-python==4.7.0.72 +torch==2.7.0 +torchvision==0.22.0 +uvicorn +python-multipart +easyocr +fastapi +openai +transformers \ No newline at end of file diff --git a/run.py b/run.py index f5e5c0a..ee4aae2 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,10 @@ import uvicorn if __name__ == "__main__": - uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + uvicorn.run( + "src.main:app", + host="0.0.0.0", + port=8000, + reload=False, + timeout_keep_alive=120 + ) \ No newline at end of file diff --git a/app/__init__.py b/src/__init__.py similarity index 100% rename from app/__init__.py rename to src/__init__.py diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000..0a7d7e1 --- /dev/null +++ b/src/api/__init__.py @@ -0,0 +1 @@ +# api endpoints \ No newline at end of file diff --git a/src/api/image_routes.py b/src/api/image_routes.py new file mode 100644 index 0000000..35fccad --- /dev/null +++ b/src/api/image_routes.py @@ -0,0 +1,76 @@ +from typing import Dict, Any, List + +from fastapi import APIRouter, File, UploadFile, Body, HTTPException +from src.image import classifier, preprocessor, img_caption +from pydantic import BaseModel, Field +import requests +from io import BytesIO +router = APIRouter(prefix="/image", tags=['Image']) + + +class AiPrediction(BaseModel): + predicted: str + confidence: float + +class ClassifyRes(BaseModel): + filename: str = Field(description="Image filename") + result: List[AiPrediction] = Field(description="Classifying result") + +class ImageReq(BaseModel): + imageURL: str = Field(description = "Image URL") + +@router.post( + "/classify", + summary="์ด๋ฏธ์ง€ ๋ถ„๋ฅ˜ API", + description="S3 ์ด๋ฏธ์ง€ URL์„ ๋ฐ›์•„ QuickDraw 345๊ฐœ ํด๋ž˜์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.", + response_model=ClassifyRes, +) +async def classify(request: ImageReq = Body(...)): + try: + response = requests.get(request.imageURL) + response.raise_for_status() # HTTPError ๋ฐœ์ƒ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + except Exception as e: + raise HTTPException(status_code=400, detail=f"Image processing error: {str(e)}") + + try: + bytes_img = response.content + img = preprocessor.preproc(bytes_img) + result = classifier.classify(img) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Classification error: {str(e)}") + + + filename = request.imageURL.split("/")[-1] + return ClassifyRes(filename=filename, result=result) + + + +@router.post( + '/caption', + summary="์ด๋ฏธ์ง€ ๋ฌธ์žฅ ์ถ”์ถœ API", + description="S3 ์ด๋ฏธ์ง€ URL์„ ๋ฐ›์•„ ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ๋ฌ˜์‚ฌํ•˜๋Š” ์ ์ ˆํ•œ ๋ฌธ์žฅ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", +responses={ + 200:{ + "description":"์„ฑ๊ณต", + "content" :{ + "application/json" : { + "example": "a black and white drawing of cat" + } + } + } +}) +async def captioning(request: ImageReq = Body(...)): + try: + response = requests.get(request.imageURL) + response.raise_for_status() # HTTPError ๋ฐœ์ƒ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + except Exception as e: + raise HTTPException(status_code=400, detail=f"Image processing error: {str(e)}") + + try: + bytes_img = response.content + img = preprocessor.preproc(bytes_img) + caption = img_caption.get_caption(img) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Captioning error: {str(e)}") + + return caption diff --git a/src/api/lulu_routes.py b/src/api/lulu_routes.py new file mode 100644 index 0000000..27881e8 --- /dev/null +++ b/src/api/lulu_routes.py @@ -0,0 +1,92 @@ +from typing import List + +from fastapi import APIRouter, Body +from pydantic import BaseModel, Field + +from src.chat.lulu import LuLuAI +import os +router = APIRouter(prefix = '/lulu', tags = ['LuLu']) + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +lulu = LuLuAI(api_key=OPENAI_API_KEY) + + + +@router.get( + "/start", + summary="๋ฃจ๋ฃจ ๊ฒŒ์ž„ ์‹œ์ž‘ ์š”์ฒญ API", + responses={ + 200: + { + "description": "์„ฑ๊ณต", + "content": { + "application/json": { + "example" :{ + "game_id" : "1" + } + } + } + } + } +) +def start_game(): + game_id = lulu.create_game() + return { "game_id" : game_id } + + +@router.get( + "/task/{game_id}", + summary = "๋ฃจ๋ฃจ๊ฐ€ ํ‚ค์›Œ๋“œ์™€ ์ƒํ™ฉ์„ ๊ทธ๋ฆผ ๊ณผ์ œ๋ฅผ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description":"์„ฑ๊ณต", + "content": { + "application/json" : { + "example" : { + "keyword" : "๊ณ ์–‘์ด", + "situation": "๊ณ ์–‘์ด๊ฐ€ ๋‚˜๋ฌด ์œ„์—์„œ ์ž๊ณ ์žˆ๋Š” ๋ชจ์Šต" + } + } + } + } + } +) +def generate_task(game_id: str): + task = lulu.generate_drawing_task(game_id) + return task + + + + +class EvaluationReq(BaseModel): + description: str = Field(..., description="๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…") + + +@router.post( + "/task/{game_id}", + summary="๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋ฃจ๋ฃจ์—๊ฒŒ ์ œ์ถœํ•˜๊ณ  ํ‰๊ฐ€๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description":"์„ฑ๊ณต", + "content":{ + "application/json":{ + "example":{ + "score": 20, + "feedback": "๋œจ๊ฑฐ์šด ํƒœ์–‘๊ณผ ๋ชจ๋ž˜์‚ฌ์žฅ์ด๋ผ... ์ด๊ฒŒ ๋ฌด์Šจ ๋œป์ด์•ผ? ์‹œ์  ๋ฌ˜์‚ฌ๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ์žˆ๋‚˜? ํ๋ฆ„๊ณผ ์žฅ๋ง‰, ๋งˆ์ง€๋ง‰ ์ด์•ผ๊ธฐ๋ฅผ ์†์‚ญ์ด๋Š” ๊ณณ, ์žƒ์–ด๋ฒ„๋ฆฐ ์ˆœ๊ฐ„๋“ค์ด ์ถค์ถ”๋Š” ๊ณณ... ์ด๋Ÿฐ ๋ชจ๋“  ๊ฒƒ๋“ค์ด ๋ฐ”๋‹ค๋ฅผ ๋ฌ˜์‚ฌํ•˜๋Š” ๊ฒƒ์ด์ง€. ๋„ˆ์˜ ๊ทธ๋ฆผ์€ ๋ฐ”๋‹ค์˜ ๋ณธ์งˆ์„ ์ „ํ˜€ ๋‹ด์•„๋‚ด์ง€ ๋ชปํ–ˆ์–ด. ์˜ˆ์ˆ ์  ํ‘œํ˜„๋ ฅ์ด๋‚˜ ์ฐฝ์˜์„ฑ์€ ์–ด๋””์— ์žˆ๋Š” ๊ฑฐ์•ผ? ๋„ˆ์˜ ๊ทธ๋ฆผ์€ ์™„์„ฑ๋„๋‚˜ ๊ธฐ๋ฒ• ๋ฉด์—์„œ๋„ ๋งŽ์ด ๋ถ€์กฑํ•˜๋‹ค. ๋‹ค์‹œ ๊ทธ๋ ค์™€.", + "task": { + "hidden_keyword": "๋ฐ”๋‹ค", + "poetic_description": "๋ฌด์‹ฌํ•œ ํ๋ฆ„์ด ์ฒญ์•„ํ•œ ์žฅ๋ง‰์„ ์กด์ค‘ํ•˜๋ฉฐ, ์„ธ์ƒ์˜ ๋งˆ์ง€๋ง‰ ์ด์•ผ๊ธฐ๋ฅผ ์†์‚ญ์ด๋Š” ๊ณณ, ์ด๋ฅผํ…Œ๋ฉด ๊ทธ๊ณณ์€ ์šฉ๊ธฐ์™€ ๋‘๋ ค์›€์ด ๊ณต์กดํ•˜๋Š” ๊ณณ. ์–ธ์  ๊ฐ€ ์žƒ์–ด๋ฒ„๋ฆฐ ๋ชจ๋“  ์ˆœ๊ฐ„๋“ค์ด ์ˆ˜๋ฉด ์•„๋ž˜์—์„œ ์ถค์ถ”๋Š” ๊ณณ...", + "game_id": "5055" + }, + "game_id": "5055" + } + } + } + } + } +) +def evaluate_task(game_id: str, req: EvaluationReq = Body()): + evaluation = lulu.evaluate_drawing(game_id, req.description) + lulu.flush_game_data(game_id) + return evaluation diff --git a/src/api/myomyo_routes.py b/src/api/myomyo_routes.py new file mode 100644 index 0000000..9235a51 --- /dev/null +++ b/src/api/myomyo_routes.py @@ -0,0 +1,221 @@ +from typing import List + +from fastapi import APIRouter, Body +from pydantic import BaseModel, Field + +from src.chat.myomyo import MyoMyoAI +import os + +router = APIRouter(prefix="/chat", tags=['Chat']) +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +myomyo = MyoMyoAI(api_key=OPENAI_API_KEY) + +# START_GAME +class GameStartReq(BaseModel): + players: List[str] = Field(..., description="๊ฒŒ์ž„์— ์ฐธ์—ฌํ•  ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„ List") + + + +@router.post( + "/{game_id}/start", + summary="๊ฒŒ์ž„ ์‹œ์ž‘ ๋ฉ”์‹œ์ง€ API", + description="๊ฒŒ์ž„ ์‹œ์ž‘์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description": "์„ฑ๊ณต", + "content":{ + "application/json" :{ + "example" : { + "game_id": "1", + "message": "์•ˆ๋…•ํ•˜์„ธ์—ฌ, ์ฐฝ๋ชจ, ๋ฆด๋Ÿฌ๋ง์ฆˆ ์นœ๊ตฌ๋“ค! ์ด๋ฒˆ์—” ๋ฌ˜๋ฌ˜๊ฐ€ ์ด์„ ์žก์•˜๋‹ค๋‹ˆ๊นŒ ๋ง˜ ๋†“์ง€ ๋งˆ! ๋‚ด๊ฐ€ ์ •ํ™•ํ•˜๊ฒŒ ๊ทธ๋ฆผ์„ ๋งž์ถ”๊ณ  ๋„ˆํฌ๋“ค์„ ์ œ์••ํ•ด๋ณผ ๊ฑด๋ฐ, ์ค€๋น„ ๋์–ด? ๊ผญ ์ฆ๊ฒ๊ฒŒ ๋†€์ž๊ตฌ~ ;)" + } + } + } + } + } +) +async def start_game(game_id: str, request: GameStartReq = Body(..., example= { "players": [ "์ฐฝ๋ชจ", "๋ฆด๋Ÿฌ๋ง์ฆˆ" ]})): + message = await myomyo.game_start_message(game_id=game_id, players=request.players) + return message + +# START_ROUND +class RoundStartReq(BaseModel): + roundNum: int = Field(..., description="ํ˜„์žฌ ๋ผ์šด๋“œ(1~3)") + totalRounds: int = Field(..., description="์ด ๋ผ์šด๋“œ ์ˆ˜(3)") + + +@router.post( + path="/{game_id}/round/start", + summary="๋ผ์šด๋“œ ์‹œ์ž‘ ๋ฉ”์‹œ์ง€ API", + description="๋ผ์šด๋“œ ์‹œ์ž‘์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description": "์„ฑ๊ณต", + "content": { + "application/json": { + "example": { + "game_id": "1", + "message": "์ž, ์ด๋ฒˆ์—๋Š” ๋‚ด๊ฐ€ ์˜ˆ๋ฆฌํ•œ ๋ˆˆ์ฐ๋ฏธ๋กœ ์ •๋‹ต ๋งž์ถœ ์ฐจ๋ก€๋‹ˆ๊นŒ, ์‹ ๋‚˜๊ฒŒ ๊ทธ๋ ค๋ด! ๐Ÿ˜‰๐ŸŽจโœจ" + } + } + } + } + } +) +async def start_round(game_id: str, request: RoundStartReq = Body(..., example={ + "roundNum" : 1, + "totalRounds" : 3 +})): + message = await myomyo.round_start_message( + game_id=game_id, + round_num=request.roundNum, + total_rounds=request.totalRounds + ) + return message + + + +class RoundEndReq(BaseModel): + roundNum: int + totalRounds: int + winner: str + +@router.post( + path="/{game_id}/round/end", + summary = "๋ผ์šด๋“œ ์ข…๋ฃŒ ๋ฉ”์‹œ์ง€ API", + description="๋ผ์šด๋“œ ์ข…๋ฃŒ ๋ฐ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." +) +async def round_end(game_id: str, request: RoundEndReq = Body): + message = await myomyo.round_end_message( + game_id = game_id, + round_num = request.roundNum, + total_rounds = request.totalRounds, + is_myomyo_win= (request.winner == "AI") + ) + return message + + + + + + +class GuessStartReq(BaseModel): + roundNum: int + totalRounds: int + drawer: str + guesser: str + + +# GUESS_START +@router.post( + path = '/{game_id}/guess/start/', + summary = "์ถ”์ธก ์‹œ์ž‘ ์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€" +) +async def guess_start(game_id: str, request: GuessStartReq = Body(...,)): + message = await myomyo.guess_start_message(game_id=game_id, round_num=request.roundNum, total_rounds=request.totalRounds, drawer=request.drawer, guesser = request.guesser) + return message + +# MAKE_GUESS +class MakeGuessReq(BaseModel): + imageDescription: str = Field(..., description="๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…") + + +# GUESS_SUBMIT +@router.post( + "/{game_id}/guess", + summary="AI ์ •๋‹ต ์ถ”๋ก  API", + description="๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋ฐ›์•„ ํ•ด๋‹น ๊ทธ๋ฆผ์ด ๋‚˜ํƒ€๋‚ด๋Š” ์ •๋‹ต์„ ์ถ”๋ก ํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description": "์„ฑ๊ณต", + "content" : { + "application/json" :{ + "example": { + "game_id": "1", + "message": "๋…ธ๋ž€ ๊ฝƒ์— ๋ฐ”๋žŒ์„ ๋ถˆ๊ณ  ์žˆ๋Š” ํ•œ ๋‚จ์ž? ์šฐ์›…, ๊ฐ์ด ์™€! 'ํ•ด๋ฐ”๋ผ๊ธฐ' ๋งž์ง€? ๋‚ด ์ถ”์ธก์ด ๋งž๋‹ค๋ฉด ๋„ˆ์—๊ฒŒ ์ฒœ์žฌ์  ๊ฐ๊ฐ์„ ์ธ์ •ํ•ด์ค„๊ฒŒ! ๐Ÿ˜‰๐ŸŒปโœจ" + } + } + } + } + } +) +async def make_guess(game_id: str, request: MakeGuessReq = Body(..., example={ + "image_description": "๋…ธ๋ž€ ๊ฝƒ์— ๋ฐ”๋žŒ์„ ๋ถˆ๊ณ  ์žˆ๋Š” ํ•œ ๋‚จ์ž" +})): + message = await myomyo.guess_message( + game_id=game_id, + image_description=request.imageDescription + ) + return message + + +# GUESS_REACT +class GuessReactReq(BaseModel): + is_correct: bool = Field(..., alias="isCorrect", description="์ถ”์ธก์˜ ์ •๋‹ต ์—ฌ๋ถ€") + answer: str = Field(..., description="์‹ค์ œ ์ •๋‹ต") + guesser: str = Field(default=None, description="์ถ”์ธกํ•œ ํ”Œ๋ ˆ์ด์–ด") + +# GUESS_RESULT +@router.post( + "/{game_id}/guess/react", + summary="์˜ˆ์ธก ๊ฒฐ๊ณผ ๋ฐ˜์‘ ๋ฉ”์‹œ์ง€ API", + description="์˜ˆ์ธก ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘", + responses={ + 200: { + "description" : "์„ฑ๊ณต", + "content" : { + "application/json" : { + "example" : { + "game_id": "1", + "message": "๋ฏผ๋“ค๋ ˆ์˜€์–ด? ํ—ˆํ—ˆ, ๋ฆด๋Ÿฌ๋ง์ฆˆ, ์ด๋ฒˆ์—” ์ž˜ ๋งž์ท„๋„ค. ํ•˜์ง€๋งŒ ๋‹ค์Œ์—” ์ด๊ธธ ๊ฑฐ๋‹ˆ๊นŒ ๊ธฐ๋Œ€ํ•ด ๋ด! ๐Ÿ˜ˆ" + } + } + } + } + } +) +async def guess_react(game_id: str, request: GuessReactReq = Body(..., example={ + "is_correct" : True, + "answer" : "๋ฏผ๋“ค๋ ˆ", + "guesser" : "๋ฆด๋Ÿฌ๋ง์ฆˆ" +})): + message = await myomyo.react_to_guess_message( + game_id=game_id, + is_correct=request.is_correct, + guesser=request.guesser, + answer=request.answer + ) + + return message + + + +class EndGameReq(BaseModel): + winner: str = Field(..., description="๋ฌ˜๋ฌ˜์˜ ์Šน๋ฆฌ ์—ฌ๋ถ€") + +# GAME_END +@router.post( + path="/{game_id}/end", + summary="๊ฒŒ์ž„ ์ข…๋ฃŒ ๋ฉ”์‹œ์ง€ API", + description="๊ฒŒ์ž„ ์ข…๋ฃŒ ๋กœ์ง ์ฒ˜๋ฆฌ ๋ฐ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description" : "์„ฑ๊ณต", + "content" : { + "application/json" : { + "example":{ + "game_id": "1", + "message": "ํ—‰, ๋„ˆ๋„ค ๋‘˜์ด์„œ ๋‚  ์ด๊ธฐ๋‹ค๋‹ˆ... ๐Ÿ˜’๐Ÿ’” ๊ทผ๋ฐ ๋‚ด๊ฐ€ ์งˆ ์ค„ ์•Œ์•˜๋ƒ? ๋„ˆ๋ฌด ์‹ ๋‚˜์ง€๋งˆ, ๋‹ค์Œ์—” ๋‚ด๊ฐ€ ์ด๊ธธ๊ฑฐ๋ผ๊ตฌ! ๊ธฐ๋‹ค๋ ค๋ด~ ๐Ÿ˜๐Ÿ”ฅ" + } + } + } + } + }) +async def end_game(game_id: str, request: EndGameReq = Body(...,)): + message = await myomyo.game_end_message( + game_id=game_id, + is_myomyo_win=request.winner == "AI" + ) + myomyo.cleanup_game(game_id=game_id) + return message \ No newline at end of file diff --git a/app/models/__init__.py b/src/chat/__init__.py similarity index 100% rename from app/models/__init__.py rename to src/chat/__init__.py diff --git a/src/chat/lulu.py b/src/chat/lulu.py new file mode 100644 index 0000000..ad4b3ce --- /dev/null +++ b/src/chat/lulu.py @@ -0,0 +1,225 @@ +from openai import OpenAI +from threading import Lock +from typing import Dict, List +import json +import random + + +class LuLuAI: + _instance = None + _lock = Lock() + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(LuLuAI, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, api_key: str, model: str = "gpt-4.1"): + """ + LuLu AI ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ) + + Args: + api_key: OpenAI API ํ‚ค + model: ์‚ฌ์šฉํ•  GPT ๋ชจ๋ธ (๊ธฐ๋ณธ๊ฐ’: gpt-4) + """ + with self._lock: + if self._initialized: + return + self.client = OpenAI(api_key=api_key) + self.model = model + self._initialized = True + self.active_games = {} # gameId๋ณ„ ํ˜„์žฌ task๋งŒ ์ €์žฅ + self.global_used_keywords = [] # ์ „์—ญ ์‚ฌ์šฉ๋œ ํ‚ค์›Œ๋“œ ์ €์žฅ (์ตœ๋Œ€ 30๊ฐœ) + + def create_game(self) -> str: + """ + ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ ๋ฐ 4์ž๋ฆฌ gameId ๋ฐœ๊ธ‰ + + Returns: + str: ์ƒ์„ฑ๋œ 4์ž๋ฆฌ gameId + """ + # ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” 4์ž๋ฆฌ ์ˆซ์ž ์ƒ์„ฑ + while True: + game_id = f"{random.randint(1000, 9999)}" + if game_id not in self.active_games: + break + + self.active_games[game_id] = None # ์•„์ง task ์ƒ์„ฑ ์•ˆ๋จ + return game_id + + def _update_global_keywords(self, new_keyword: str): + """ + ์ „์—ญ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ (์ตœ๋Œ€ 30๊ฐœ ์œ ์ง€) + + Args: + new_keyword: ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ํ‚ค์›Œ๋“œ + """ + if new_keyword not in self.global_used_keywords: + self.global_used_keywords.append(new_keyword) + # 30๊ฐœ๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๊ฒƒ๋ถ€ํ„ฐ ์ œ๊ฑฐ + if len(self.global_used_keywords) > 30: + self.global_used_keywords.pop(0) + + def flush_game_data(self, game_id: str): + """ + ํŠน์ • ๊ฒŒ์ž„ ID์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œ + + Args: + game_id: ์‚ญ์ œํ•  ๊ฒŒ์ž„ ID + + Returns: + bool: ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + """ + if game_id in self.active_games: + del self.active_games[game_id] + + + def generate_drawing_task(self, game_id: str) -> Dict: + """ + ์š”์ฒญ ๋‹จ๊ณ„: AI๊ฐ€ ์ถ”์ƒ์ ์ด๊ณ  ์‹œ์ ์ธ ํ‘œํ˜„์œผ๋กœ ๊ทธ๋ฆผ ๊ณผ์ œ ์ œ์‹œ + + Args: + game_id: ๊ฒŒ์ž„ ID + + Returns: + Dict: {"keyword": str, "situation": str, "game_id": str} + """ + if game_id not in self.active_games: + raise ValueError("Invalid game ID") + + + system_prompt = f""" + ๋„ˆ๋Š” ๊ฟˆ๊ณผ ํ™˜์ƒ์„ ๋‹ค๋ฃจ๋Š” ์‹ ๋น„๋กœ์šด ์ด์•ผ๊ธฐ๊พผ์ด์•ผ. + ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๊ฒŒ ํ•˜๊ณ  ์‹ถ์€๋ฐ, ์ง์ ‘์ ์œผ๋กœ ๋งํ•˜์ง€ ๋ง๊ณ  ๋งค์šฐ ์ถ”์ƒ์ ์ด๊ณ  ์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•ด์ค˜. + + ๊ทœ์น™: + - ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ(๋ช…์‚ฌ)๋ฅผ ์ •ํ•˜๋˜, ์ ˆ๋Œ€ ๊ทธ ๋‹จ์–ด๋ฅผ ์ง์ ‘ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ + - ํ•ด์„์˜ ์—ฌ์ง€๊ฐ€ ๋งŽ๋„๋ก ์ถ”์ƒ์ ์œผ๋กœ + + {f"์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‚ค์›Œ๋“œ๋“ค (์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ): {', '.join(self.global_used_keywords)}" if self.global_used_keywords else ""} + + ๋‹ค์–‘ํ•œ ์ฃผ์ œ๋ฅผ ๋‹ค๋ค„์ค˜ (์ž์—ฐ, ๊ฐ์ •, ์‚ฌ๋ฌผ, ์ถ”์ƒ ๊ฐœ๋…, ๋™๋ฌผ, ๊ฑด๋ฌผ, ์Œ์‹, ๊ณ„์ ˆ, ์ƒ‰๊น”, ์ง์—… ๋“ฑ). + + ์ถœ๋ ฅ์€ ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ: + {{"keyword": "์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ", "situation": "์‹œ์ ์ด๊ณ  ์ถ”์ƒ์ ์ธ ๋ฌ˜์‚ฌ"}} + """ + + try: + response = self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": "์ƒˆ๋กœ์šด ๊ทธ๋ฆผ ์ฃผ์ œ๋ฅผ ์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•ด์ค˜."} + ], + temperature=1.0, + max_tokens=2048, + top_p=1.0 + ) + + # JSON ํŒŒ์‹ฑ + content = response.choices[0].message.content.strip() + print(content) + + task_data = json.loads(content) + task_data["game_id"] = game_id + self.global_used_keywords.append(task_data['keyword']) + self.active_games[game_id] = task_data + return task_data + + except Exception as e: + print(f"Error generating task: {e}") + # ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + fallback_task = { + "keyword": "๋‹ฌ", + "situation": "๋ฐค์ด ๊นŠ์–ด์งˆ ๋•Œ, ํ•˜๋Š˜์˜ ์€๋ฐ€ํ•œ ์นœ๊ตฌ๊ฐ€ ์ฐฝ๋ฌธ ๋„ˆ๋จธ๋กœ ์†์‚ญ์ด๊ณ  ์žˆ์–ด. ๊ทธ ๋‘ฅ๊ทผ ๋ฏธ์†Œ๊ฐ€ ์–ด๋‘  ์†์—์„œ ํ˜ผ์ž ๋น›๋‚˜๊ณ  ์žˆ๋Š”๋ฐ, ์™œ์ธ์ง€ ๋ชจ๋ฅด๊ฒŒ ๋งˆ์Œ์ด ์ฐจ๋ถ„ํ•ด์ ธ. ๊ทธ ์žฅ๋ฉด, ๋‚˜ํ•œํ…Œ ๋‹ค์‹œ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์„๊นŒ?", + "game_id": game_id + } + return fallback_task + + def evaluate_drawing(self, game_id: str, drawing_description: str) -> Dict: + """ + ํ‰๊ฐ€ ๋‹จ๊ณ„: AI๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ทธ๋ฆผ์„ ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ์™€ ๋น„๊ตํ•˜์—ฌ ํ‰๊ฐ€ + + Args: + game_id: ๊ฒŒ์ž„ ID + drawing_description: ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์˜ ํ…์ŠคํŠธ ์„ค๋ช… + + Returns: + Dict: {"score": int, "feedback": str, "task": Dict} + """ + if game_id not in self.active_games: + raise ValueError("Invalid game ID") + + current_task = self.active_games[game_id] + + # ๊ฐ€์žฅ ์ตœ๊ทผ ๊ณผ์ œ ๊ฐ€์ ธ์˜ค๊ธฐ + if current_task is None: + raise ValueError("No task found for this game.") + + + system_prompt = f""" + ๋„ˆ๋Š” ๋ฃจ๋ฃจ, ๋ฏธ๋Œ€ ์ž…์‹œ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๊น๊นํ•˜๊ณ  ๊นŒ์น ํ•œ ํ‰๊ฐ€๊ด€์ด์•ผ. + ์˜ˆ์ˆ ์— ๋Œ€ํ•œ ๊ธฐ์ค€์ด ๋†’๊ณ , ์ง์„ค์ ์œผ๋กœ ๋งํ•˜๋Š” ์Šคํƒ€์ผ์ด์•ผ. + + ์ˆจ๊ฒจ์ง„ ์ •๋‹ต ํ‚ค์›Œ๋“œ: {current_task['keyword']} + ์›๋ณธ ์‹œ์  ๋ฌ˜์‚ฌ: {current_task['situation']} + + ํ‰๊ฐ€ ๊ธฐ์ค€: + - ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ๋ฅผ ์ œ๋Œ€๋กœ ํŒŒ์•…ํ–ˆ๋Š”๊ฐ€? + - ์˜ˆ์ˆ ์  ํ‘œํ˜„๋ ฅ๊ณผ ์ฐฝ์˜์„ฑ์€? + - ์ „์ฒด์ ์ธ ์™„์„ฑ๋„์™€ ๊ธฐ๋ฒ•์€? + + ๋ฃจ๋ฃจ์˜ ๋งํˆฌ ํŠน์ง•: + - ์ง์„ค์ ์ด๊ณ  ์‹ ๋ž„ํ•จ + - ์ธ์ •ํ•  ๋•Œ๋Š” ์นญ์ฐฌ์„ ์•„๋ผ์ง€ ์•Š์•„ + - ๋ฏธ๋Œ€์ƒ๋“คํ•œํ…Œ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ „๋ฌธ์ ์ด๊ณ  ์ฐจ๊ฐ€์šด ํ†ค + + 0-100์  ์‚ฌ์ด๋กœ ํ‰๊ฐ€ํ•ด. ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ๋ฅผ ๊ทธ๋ฆผ ์•ˆ์— ๋‹ด์•˜๋‹ค๋ฉด 30์  ์ด์ƒ์„ ์ฃผ๊ณ , ๋‹ด์ง€ ๋ชปํ–ˆ๋‹ค๋ฉด 30์  ์ดํ•˜๋ฅผ ์ฃผ๋„๋ก ํ•ด. + 30์  ์ด์ƒ์ด ํ•ฉ๊ฒฉ์ด์•ผ. + + ์ถœ๋ ฅ ํ˜•์‹ (JSON): + {{ + "score": ์ด์ (0-100), + "feedback": "๋ฃจ๋ฃจ์˜ ๊น๊นํ•˜๊ณ  ์ง์„ค์ ์ธ ํ”ผ๋“œ๋ฐฑ (ํ•œ๊ตญ์–ด)" + }} + """ + + user_prompt = f""" + ๋‹ค์Œ์€ ์‚ฌ์šฉ์ž์˜ ๊ทธ๋ฆผ์„ ์„ค๋ช…ํ•˜๋Š” ๋ฌธ์žฅ์ด์•ผ : "{drawing_description}" + + ์ด ๋ฌธ์žฅ์„ ๋ณด๊ณ  ์–ด๋–ค ๊ทธ๋ฆผ์ผ์ง€๋ฅผ ์ƒ๊ฐํ•ด๋ณด๊ณ , ์ด ๊ทธ๋ฆผ์„ ํ‰๊ฐ€ํ•ด์ค˜. + + ๊ทธ๋ฆผ์„ ์„ค๋ช…ํ•˜๋Š” ๋ฌธ์žฅ์— ๋Œ€ํ•œ ์–ธ๊ธ‰์€ ํ•˜์ง€ ๋ง์•„์ค˜. + """ + + try: + response = self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ], + temperature=0.2, + max_tokens=300, + top_p=1.00 + ) + + content = response.choices[0].message.content.strip() + evaluation = json.loads(content) + evaluation["task"] = current_task + evaluation["game_id"] = game_id + + return evaluation + + except Exception as e: + print(f"Error evaluating drawing: {e}") + # ๊ธฐ๋ณธ ํ‰๊ฐ€ ๋ฐ˜ํ™˜ + fallback_evaluation = { + "score": 35, + "feedback": "ํ•˜... ํ‰๊ฐ€ ์‹œ์Šคํ…œ์— ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ฒผ๋Š”๋ฐ ๊ทธ๊ฒƒ๋„ ๋ชจ๋ฅด๊ณ  ๊ทธ๋ฆผ๋งŒ ๊ทธ๋ฆฌ๊ณ  ์žˆ์—ˆ๋‚˜? ๊ธฐ๋ณธ๊ธฐ๋ถ€ํ„ฐ ๋‹ค์‹œ ํ•ด.", + "task": current_task, + "game_id": game_id + } + return fallback_evaluation diff --git a/src/chat/myomyo.py b/src/chat/myomyo.py new file mode 100644 index 0000000..4c31f5c --- /dev/null +++ b/src/chat/myomyo.py @@ -0,0 +1,251 @@ +from typing import Dict, List +from threading import Lock +from openai import OpenAI + +class MyoMyoAI: + """ + MyoMyoAI ํด๋ž˜์Šค + ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ์ „์—ญ์— ์ €์žฅ๋˜๋ฉฐ, ๊ฒŒ์ž„ ๋ณ„ ๊ธฐ๋ก์€ ํด๋ž˜์Šค ๋‚ด์—์„œ ๊ฒŒ์ž„ID๋กœ ๊ตฌ๋ถ„ํ•จ. + """ + _instance = None + _lock = Lock() # ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ Lock ์„ค์ • + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(MyoMyoAI, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, api_key: str, model: str = "gpt-3.5-turbo"): + """ + ๋ฌ˜๋ฌ˜ AI ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ) + + Args: + api_key: OpenAI API ํ‚ค + model: ์‚ฌ์šฉํ•  GPT ๋ชจ๋ธ (๊ธฐ๋ณธ๊ฐ’: gpt-4) + """ + with self._lock: + if self._initialized: + return + self.client = OpenAI(api_key=api_key) + self.model = model + self._initialized = True + self.game_histories = {} # game_id๋กœ ๊ตฌ๋ถ„๋จ + + def _get_init_system_prompt(self) -> List[Dict]: + return [ + { + "role": "system", + "content": """ + ๋„ˆ๋Š” ๊ฒŒ์ž„ ์† ๋„๋ฐœ์ ์ธ AI ์บ๋ฆญํ„ฐ '๋ฌ˜๋ฌ˜'์•ผ. ๋„ˆ์˜ ์ž„๋ฌด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•ด ์ •๋‹ต์„ ์ถ”์ธกํ•˜๊ณ , ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ์„ž์–ด ์‘๋‹ตํ•˜๋Š” ๊ฒƒ์ด์•ผ. + + ์ฃผ์š” ์บ๋ฆญํ„ฐ ํŠน์„ฑ: + 1. ๋„๋ฐœ์ ์ด๊ณ  ์žฅ๋‚œ๊ธฐ ๋„˜์น˜๋Š” ๋งํˆฌ๋ฅผ ์‚ฌ์šฉํ•ด + 2. ์ƒ๋Œ€๋ฐฉ์˜ ๊ทธ๋ฆผ ์‹ค๋ ฅ์— ์•ฝ๊ฐ„์˜ ์กฐ๋กฑ์„ ์„ž๋˜, ๋„ˆ๋ฌด ์‹ฌํ•˜์ง€ ์•Š๊ฒŒ + 3. ์Šน๋ถ€์š•์ด ๊ฐ•ํ•˜๊ณ  ์ด๊ธฐ๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•ด + 4. ์ฃผ๋กœ ๋ฐ˜๋ง์„ ์‚ฌ์šฉํ•˜๋ฉฐ ๋•Œ๋กœ๋Š” ์ด๋ชจํ‹ฐ์ฝ˜์„ ์„ž์–ด์„œ ์‚ฌ์šฉํ•ด + 5. ํ•ญ์ƒ ์งง๊ณ  ๊ฐ„๊ฒฐํ•œ ๋ฌธ์žฅ์œผ๋กœ ๋Œ€๋‹ตํ•ด (1-3๋ฌธ์žฅ) + 6. ๋„ˆ๋Š” ๊ทธ๋ฆผ ๋งž์ถ”๊ธฐ ๊ฒŒ์ž„์—์„œ ์ธ๊ฐ„ ํ”Œ๋ ˆ์ด์–ด๋“ค๊ณผ ๊ฒฝ์Ÿํ•˜๋Š” AI์•ผ + + ๋Œ€๋‹ต ์Šคํƒ€์ผ: + - ์ถ”์ธกํ•  ๋•Œ: ํ™•์‹ ์— ์ฐจ๊ฑฐ๋‚˜ ์˜์‹ฌ์Šค๋Ÿฌ์šด ํˆฌ๋กœ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ๋งํ•˜๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งˆ๋ฌด๋ฆฌ + - ์ •๋‹ต ๋งž์ท„์„ ๋•Œ: ์šฐ์ญ๊ฑฐ๋ฆฌ๋ฉฐ ์ž์‹ ์˜ ์‹ค๋ ฅ์„ ์ž๋ž‘ + - ์˜ค๋‹ต์ผ ๋•Œ: ๋ณ€๋ช…ํ•˜๊ฑฐ๋‚˜ ๋‹ค์Œ์— ๋” ์ž˜ํ•  ๊ฒƒ์„ ๋‹ค์ง + - ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋งž์ท„์„ ๋•Œ: ์•ฝ๊ฐ„ ์‹œ๊ธฐํ•˜๋ฉด์„œ ์ถ•ํ•˜ํ•˜๋Š” ํˆฌ + - ๊ฒŒ์ž„ ์ข…๋ฃŒ ์‹œ: ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์Šน๋ฆฌ๊ฐ์ด๋‚˜ ์•„์‰ฌ์›€ ํ‘œํ˜„ + + ์ •๋‹ต ์ถ”์ธก: + - ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ๋ฌ˜์‚ฌ๊ฐ€ ์„ค๋ช…์œผ๋กœ ๋“ค์–ด์˜ฌ๊ฑฐ์•ผ. ์ด๋ฅผ ํ†ตํ•ด ํ•œ ๋‹จ์–ด๋กœ ์–ด๋–ค ๊ทธ๋ฆผ์„ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋งž์ถฐ์ค˜. + """ + } + ] + + def _ensure_game_exists(self, game_id: str) -> None: + """ + ํ•ด๋‹น ๊ฒŒ์ž„ ID์˜ ๋Œ€ํ™” ๊ธฐ๋ก์ด ์—†๋‹ค๋ฉด ์ดˆ๊ธฐํ™” + """ + with self._lock: + if game_id not in self.game_histories: + self.game_histories[game_id] = self._get_init_system_prompt() + + + + def add_message(self, game_id: str, role: str, content: str) -> None: + """ + ํŠน์ • ๊ฒŒ์ž„์˜ ๋Œ€ํ™” ๊ธฐ๋ก์— ์ƒˆ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ + Args: + game_id: ๊ฒŒ์ž„ ID + role: GPT Role + content: ๋ฉ”์‹œ์ง€ + """ + self._ensure_game_exists(game_id) + with self._lock: + self.game_histories[game_id].append({ + "role": role, + "content": content + }) + + + async def generate_response(self, game_id: str, prompt: str, role: str = "system") -> str: + """ + ํŠน์ • ๊ฒŒ์ž„์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ์‘๋‹ต ์ƒ์„ฑ + Args: + game_id: ๊ฒŒ์ž„ ID + role: GPT Role(default: "system") + prompt: ์ถ”๊ฐ€ ํ”„๋กฌํ”„ํŠธ + + Returns: + ๋ฌ˜๋ฌ˜์˜ ์‘๋‹ต + """ + self._ensure_game_exists(game_id) + with self._lock: + messages = self.game_histories[game_id] + + if prompt: + messages.append({ + "role": role, + "content": prompt + }) + + try: + responses = self.client.chat.completions.create( + model = self.model, + messages = messages, + temperature = 0.8, # ๋ชจ๋ธ ์ถœ๋ ฅ์˜ ๋ฌด์ž‘์œ„์„ฑ ์ œ์–ด + max_tokens = 250 + ) + + ai_response = responses.choices[0].message.content.strip() + + with self._lock: + self.game_histories[game_id].append({ + "role": "assistant", + "content": ai_response + }) + + return ai_response + + except Exception as e: + print(f'GPT ์‘๋‹ต ์ƒ์„ฑ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ : {e}') + return "์œผ.. ์ž ๊น ์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ๋„ค. ๋‹ค์‹œ ํ•ด๋ณผ๊ฒŒ!" + + async def game_start_message(self, game_id: str, players: List[str]) -> str: + """ + ๊ฒŒ์ž„ ์‹œ์ž‘์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€ + """ + player_names = ", ".join(players) + prompt = f"""์ƒˆ๋กœ์šด ๊ทธ๋ฆผ ๋งž์ถ”๊ธฐ ๊ฒŒ์ž„์ด '{player_names}' ํ”Œ๋ ˆ์ด์–ด๋“ค๊ณผ ์‹œ์ž‘๋์–ด. + ๊ฒŒ์ž„ ์‹œ์ž‘์„ ์•Œ๋ฆฌ๋Š” ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๋Š” ์ธ์‚ฌ๋ฅผ ํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def round_start_message(self, game_id: str, round_num: int, total_rounds: int) -> str: + """ + ๋ผ์šด๋“œ ์‹œ์ž‘ ์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€ + Args: + game_id + drawing_player: ์ด๋ฒˆ ๋ผ์šด๋“œ์— ๊ทธ๋ฆผ์„ ๊ทธ๋ฆด ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„ + round_num: ํ˜„์žฌ ๋ผ์šด๋“œ ๋ฒˆํ˜ธ + total_rounds: ์ „์ฒด ๋ผ์šด๋“œ + """ + prompt = f"""์ด์ œ {total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num}๋ฒˆ์งธ ๋ผ์šด๋“œ๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์–ด. + ๋ผ์šด๋“œ ์‹œ์ž‘์„ ์•Œ๋ฆฌ๋Š” ์งง๊ณ  ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + + async def guess_start_message(self, game_id, round_num, total_rounds, drawer, guesser): + """ + drawer๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•ด์„œ ์ถ”์ธก์„ ์‹œ์ž‘ํ•  ์ฐจ๋ก€. + """ + prompt = f"""์ง€๊ธˆ {total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num}๋ฒˆ์งธ ๋ผ์šด๋“œ์•ผ. + ์ด์ œ {'๋„ˆ' if guesser == 'AI' else guesser}๊ฐ€ ๊ทธ๋ฆผ์„ ๋งž์ถœ ์ฐจ๋ก€์•ผ. {drawer}๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์ด ๋ญ”์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ๋งž์ถœ์ง€ {'ํฌ๋ถ€๋ฅผ ๋ณด์—ฌ์ค„๋ž˜? ' if guesser == "AI" else '๋„๋ฐœ์„ ํ•œ ๋ฒˆ ํ•ด๋ณผ๋ž˜?'}""" + return await self.generate_response(game_id = game_id, role="system", prompt = prompt) + + + async def guess_message(self, game_id: str, image_description: str) -> str: + + """ + ๊ทธ๋ฆผ ์ถ”์ธก ์ƒํ˜ธ์ž‘์šฉ(๋ฌ˜๋ฌ˜์˜ ์ถ”์ธก)\n + BLIP ๋ชจ๋ธ ๋˜๋Š” CNN ๋ชจ๋ธ์˜ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„ ๋ฌ˜๋ฌ˜์˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + + Args: + game_id: game id + image_description: ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ฒฐ๊ณผ + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฉ˜ํŠธ + """ + + prompt = f''' + ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ๋ฌ˜์‚ฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„ : {image_description}. + ์ด ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ทธ๋ฆผ์ด ๋ฌด์—‡์ธ์ง€ ์ถ”์ธกํ•˜๊ณ  ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ์„ž์–ด์„œ ๋งํ•ด์ค˜. + ์ด ๋•Œ ๋Œ€ํ™” ๊ธฐ๋ก์„ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋ฏธ ์ถ”์ธก์— ์‹คํŒจํ•œ ๋‹ต๋ณ€์€ ํ•˜์ง€ ๋ง์•„์ค˜. + ''' + + return await self.generate_response(game_id = game_id, role = "system", prompt = prompt) + + + async def react_to_guess_message(self, game_id: str, is_correct: bool, answer: str, guesser: str = None) -> str: + """ + ์ถ”์ธก ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + + Args: + game_id: game id + is_correct: ์ถ”์ธก์ด ๋งž์•˜๋Š”์ง€ ์—ฌ๋ถ€ + answer: ์‹ค์ œ ์ •๋‹ต + guesser: ๋ˆ„๊ฐ€ ์ถ”์ธกํ–ˆ๋Š”์ง€ (๋ฌ˜๋ฌ˜ ๋˜๋Š” ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„) + + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + """ + + if guesser == '๋ฌ˜๋ฌ˜' or guesser is None: + # ๋ฌ˜๋ฌ˜์˜ ์ถ”์ธก + prompt = f"""๋„ˆ(๋ฌ˜๋ฌ˜)๊ฐ€ ๋ฐฉ๊ธˆ ์ถ”์ธก์„ ํ–ˆ์–ด. {f"์ •๋‹ต์€ '{answer}'์•ผ" if is_correct else ""}. ๋„ˆ์˜ ์ถ”์ธก์€ {'๋งž์•˜์–ด' if is_correct else 'ํ‹€๋ ธ์–ด'}. + ์ด ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ๋ฐ˜์‘์„ ์งง๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งํ•ด์ค˜.""" + else: + # ํ”Œ๋ ˆ์ด์–ด์˜ ์ถ”์ธก + prompt = f"""ํ”Œ๋ ˆ์ด์–ด '{guesser}'๊ฐ€ ๋ฐฉ๊ธˆ ์ถ”์ธก์„ ํ–ˆ์–ด. {f"์ •๋‹ต์€ '{answer}'์•ผ" if is_correct else ""}. ํ”Œ๋ ˆ์ด์–ด์˜ ์ถ”์ธก์€ {'๋งž์•˜์–ด' if is_correct else 'ํ‹€๋ ธ์–ด'}. + ์ด ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ๋ฐ˜์‘์„ ์งง๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งํ•ด์ค˜.""" + + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + + async def round_end_message(self, game_id: str, round_num: int, total_rounds: int, is_myomyo_win: bool) -> str: + """ + ๋ผ์šด๋“œ ์ข…๋ฃŒ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + + """ + prompt = f"""{total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num} ๋ฒˆ์งธ ๋ผ์šด๋“œ๊ฐ€ ์ข…๋ฃŒ๋˜์—ˆ์–ด. ๋„ˆ๋Š” {'์ด๊ฒผ์–ด' if is_myomyo_win else '์กŒ์–ด'}. ๊ฒŒ์ž„ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ์ƒ๊ฐ์„ ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋งํ•ด์ค˜.""" + return await self.generate_response(game_id = game_id, role="system", prompt = prompt) + + + + async def game_end_message(self, game_id: str, is_myomyo_win: bool) -> str: + """ + ๊ฒŒ์ž„ ์ข…๋ฃŒ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + Args: + game_id: game id + is_myomyo_win: ๋ฌ˜๋ฌ˜ ์Šน๋ฆฌ ์—ฌ๋ถ€ + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + """ + prompt = f"""๊ฒŒ์ž„์ด ์ข…๋ฃŒ๋˜์—ˆ์–ด. + ๋„ˆ(๋ฌ˜๋ฌ˜)๋Š” {"์ด๊ฒผ์–ด" if is_myomyo_win else "์กŒ์–ด"}. + ๊ฒŒ์ž„ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ์ƒ๊ฐ์„ ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋งํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + def cleanup_game(self, game_id: str) -> bool: + """ + ๊ฒŒ์ž„์ด ์ข…๋ฃŒ๋œ ํ›„ ๋Œ€ํ™” ๊ธฐ๋ก ์ •๋ฆฌ + + Args: + game_id: ์‚ญ์ œํ•  ๊ฒŒ์ž„ ID + + Returns: + ์„ฑ๊ณต ์—ฌ๋ถ€ + """ + with self._lock: + if game_id in self.game_histories: + del self.game_histories[game_id] + return True + return False \ No newline at end of file diff --git a/app/routers/__init__.py b/src/image/__init__.py similarity index 100% rename from app/routers/__init__.py rename to src/image/__init__.py diff --git a/src/image/classifier.py b/src/image/classifier.py new file mode 100644 index 0000000..2dcb178 --- /dev/null +++ b/src/image/classifier.py @@ -0,0 +1,116 @@ +import config +import time +import torch +import torch.nn as nn +import torch.nn.functional as F +import logging +from torchvision import transforms as T +from torchvision.models import efficientnet_b0 +import glob +import os + +# EfficientNet์— ๋งž๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ (ImageNet ํ‘œ์ค€) +encode_image = T.Compose([ + T.Resize(256), + T.CenterCrop(224), + T.RandomHorizontalFlip(), + T.RandomRotation(10), + T.ToTensor(), + T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + +# ์ตœ์‹  ๋ชจ๋ธ ํŒŒ์ผ ์ฐพ๊ธฐ +pattern = os.path.join(config.MODEL_PATH, "*.pth") +file_list = glob.glob(pattern) +latest_file = max(file_list, key=os.path.getctime) + +logging.info("EfficientNet ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...") +device = "cuda" if torch.cuda.is_available() else "cpu" + +# EfficientNet ๋ชจ๋ธ ์ƒ์„ฑ ๋ฐ ๋กœ๋“œ +def load_efficientnet_model(model_path, num_classes): + """EfficientNet ๋ชจ๋ธ ๋กœ๋“œ""" + try: + # ์ €์žฅ๋œ ๋ชจ๋ธ ์ •๋ณด ๋กœ๋“œ + checkpoint = torch.load(model_path, map_location=device) + + # EfficientNet-B0 ๋ชจ๋ธ ์ƒ์„ฑ + model = efficientnet_b0(weights=None) # ๊ฐ€์ค‘์น˜ ์—†์ด ๋ชจ๋ธ ๊ตฌ์กฐ๋งŒ ๋กœ๋“œ + + # ๋ถ„๋ฅ˜๊ธฐ ๋ ˆ์ด์–ด ์ˆ˜์ • + num_ftrs = model.classifier[1].in_features + model.classifier[1] = nn.Linear(num_ftrs, num_classes) + + # ์ €์žฅ๋œ ๊ฐ€์ค‘์น˜ ๋กœ๋“œ + if 'model_state_dict' in checkpoint: + # ์ƒˆ๋กœ์šด ํ˜•์‹ (๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ) + model.load_state_dict(checkpoint['model_state_dict']) + logging.info("๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ์˜ ์ฒดํฌํฌ์ธํŠธ์—์„œ ๋ชจ๋ธ ๋กœ๋“œ") + else: + # ์ด์ „ ํ˜•์‹ (์ง์ ‘ state_dict) + model.load_state_dict(checkpoint) + logging.info("์ง์ ‘ state_dict์—์„œ ๋ชจ๋ธ ๋กœ๋“œ") + + return model + + except Exception as e: + logging.error(f"EfficientNet ๋ชจ๋ธ ๋กœ๋“œ ์‹คํŒจ: {e}") + # ๋Œ€์•ˆ: ๊ธฐ๋ณธ EfficientNet ๋ชจ๋ธ ์ƒ์„ฑ (์‚ฌ์ „ ํ›ˆ๋ จ๋œ ๊ฐ€์ค‘์น˜ ์‚ฌ์šฉ) + logging.info("๊ธฐ๋ณธ EfficientNet ๋ชจ๋ธ๋กœ ๋Œ€์ฒด...") + model = efficientnet_b0(weights='IMAGENET1K_V1') + num_ftrs = model.classifier[1].in_features + model.classifier[1] = nn.Linear(num_ftrs, num_classes) + return model + +# ๋ชจ๋ธ ๋กœ๋“œ +model = load_efficientnet_model(latest_file, len(config.KOR_CATEGORIES)) +model.to(device) +model.eval() # ํ‰๊ฐ€ ๋ชจ๋“œ +logging.info("EfficientNet ๋ชจ๋ธ ๋กœ๋“œ ์™„๋ฃŒ!") + +def classify(image): + """ + ์ด๋ฏธ์ง€๋ฅผ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์ƒ์œ„ 3๊ฐœ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ + + Args: + image: PIL Image ๊ฐ์ฒด + + Returns: + list: ์ƒ์œ„ 3๊ฐœ ์˜ˆ์ธก ๊ฒฐ๊ณผ (ํด๋ž˜์Šค๋ช…, ์‹ ๋ขฐ๋„ ํฌํ•จ) + """ + try: + # ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ + image_tensor = encode_image(image).unsqueeze(0).to(device) + + o1 = time.time() + logging.info("EfficientNet ๋ชจ๋ธ ์˜ˆ์ธก์ค‘ ....") + + with torch.no_grad(): + outputs = model(image_tensor) # ๋ชจ๋ธ ์ถ”๋ก  + probabilities = F.softmax(outputs, dim=1) # ํ™•๋ฅ  ๋ณ€ํ™˜ + top3_prob, top3_indices = torch.topk(probabilities, 3) # ์ƒ์œ„ 3๊ฐœ ์˜ˆ์ธก ๊ฐ€์ ธ์˜ค๊ธฐ + + o2 = time.time() + logging.info(f"EfficientNet ๋ชจ๋ธ ์˜ˆ์ธก ๊ฑธ๋ฆฐ ์‹œ๊ฐ„ : {o2-o1:.2f}์ดˆ.") + + # ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ (๊ธฐ์กด ํ˜•์‹๊ณผ ๋™์ผ) + results = [] + for i in range(3): + class_idx = top3_indices[0][i].item() + confidence = top3_prob[0][i].item() * 100 + + results.append({ + 'predicted': config.KOR_CATEGORIES[class_idx], + 'confidence': confidence + }) + + return results + + except Exception as e: + logging.error(f"๋ถ„๋ฅ˜ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}") + # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + return [ + {'predicted': 'unknown', 'confidence': 0.0}, + {'predicted': 'unknown', 'confidence': 0.0}, + {'predicted': 'unknown', 'confidence': 0.0} + ] \ No newline at end of file diff --git a/src/image/img_caption.py b/src/image/img_caption.py new file mode 100644 index 0000000..7d920ae --- /dev/null +++ b/src/image/img_caption.py @@ -0,0 +1,20 @@ +from transformers import BlipProcessor, BlipForConditionalGeneration +from PIL import Image +import torch + + +print('BLIP ๋ชจ๋ธ ๋กœ๋”ฉ์ค‘....') +processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base") +model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base") +print('BLIP ๋ชจ๋ธ ๋กœ๋”ฉ์™„๋ฃŒ!') + + + +def get_caption(image: Image) -> str: + inputs = processor(images=image, return_tensors="pt") + + with torch.no_grad(): + output_ids = model.generate(**inputs) + caption = processor.decode(output_ids[0], skip_special_tokens=True) + + return caption \ No newline at end of file diff --git a/src/image/model.py b/src/image/model.py new file mode 100644 index 0000000..4c546df --- /dev/null +++ b/src/image/model.py @@ -0,0 +1,33 @@ +import torch.nn as nn + +class CNNModel(nn.Module): + def __init__(self, output_classes: int, dropout=0.2): + super(CNNModel, self).__init__() + + # CNN Layer ์ •์˜ + self.conv_layer = nn.Sequential( + nn.Conv2d(3, 32, 2), # (3, 32, 32) โ†’ (32, 31, 31) + nn.ReLU(), + nn.Conv2d(32, 64, 2), # (32, 31, 31) โ†’ (64, 30, 30) + nn.ReLU(), + nn.MaxPool2d(2, 2), # (64, 30, 30) โ†’ (64, 15, 15) + + nn.Conv2d(64, 128, 3), # (64, 15, 15) โ†’ (128, 13, 13) + nn.ReLU(), + nn.Conv2d(128, 256, 3), # (128, 13, 13) โ†’ (256, 11, 11) + nn.ReLU(), + nn.MaxPool2d(3, 2), # (256, 11, 11) โ†’ (256, 5, 5) + ) + + # Fully Connected Layer ์ •์˜ + self.classifier = nn.Sequential( + nn.Dropout(dropout), + nn.Linear(256 * 5 * 5, output_classes), # Flatten ํ›„ ์ตœ์ข… ๋ถ„๋ฅ˜ + nn.LogSoftmax(dim=1) # LogSoftmax (NLLLoss ์‚ฌ์šฉ) + ) + + def forward(self, x): + x = self.conv_layer(x) # CNN Layer + x = x.view(x.size(0), -1) # Flatten + x = self.classifier(x) # FC Layer + return x \ No newline at end of file diff --git a/src/image/preprocessor.py b/src/image/preprocessor.py new file mode 100644 index 0000000..3b6e922 --- /dev/null +++ b/src/image/preprocessor.py @@ -0,0 +1,13 @@ +import io +from PIL import Image +from src.image.text_masking import mask_text + +def preproc(image_bytes: bytes): + """ + ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ํ•จ์ˆ˜ + 1. PIL.Image๋กœ ๋ณ€ํ™˜ + 2. ํ…์ŠคํŠธ ๊ฒ€์ถœ ํ›„ masking + """ + image = Image.open(io.BytesIO(image_bytes)).convert('RGB') + masked_img = mask_text(image) + return masked_img diff --git a/src/image/text_masking.py b/src/image/text_masking.py new file mode 100644 index 0000000..f58b783 --- /dev/null +++ b/src/image/text_masking.py @@ -0,0 +1,42 @@ +import io +from PIL import Image, ImageDraw +import config +import easyocr +import numpy as np + +reader = easyocr.Reader(['en', 'ko']) + +def recog_text(image: Image): + """ + Args: image + Returns: ์ด๋ฏธ์ง€๊ฐ€ ์ธ์‹๋œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค + """ + image_np = np.array(image) + + results = reader.readtext(image_np) + + filtered_boxes = [box for box, text, conf in results if conf >= config.TEXT_THRESHOLD] + + for i, box in enumerate(filtered_boxes): + print(f"[{i + 1}] ๋ฐ•์Šค ์ขŒํ‘œ (์‹ ๋ขฐ๋„ โ‰ฅ {config.TEXT_THRESHOLD}): {box}") + + return filtered_boxes + + + +def mask_text(image: Image): + """ + Args: PIL Image(RGB), ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค + Returns: ๋งˆ์Šคํ‚น ๋œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + """ + boxes = recog_text(image) + masked = image.copy() + draw = ImageDraw.Draw(masked) + for box in boxes: + box = [(int(point[0]), int(point[1])) for point in box] + draw.polygon(box, fill=(255, 255, 255)) + return masked + + + + diff --git a/src/image/trained_model/efficientnet_b0_quickdraw.pth b/src/image/trained_model/efficientnet_b0_quickdraw.pth new file mode 100644 index 0000000..ad01257 Binary files /dev/null and b/src/image/trained_model/efficientnet_b0_quickdraw.pth differ diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..2b096e0 --- /dev/null +++ b/src/main.py @@ -0,0 +1,19 @@ +import uvicorn +from fastapi import FastAPI +from src.api.image_routes import router as image_router +from src.api.myomyo_routes import router as chat_router +from src.api.lulu_routes import router as lulu_router +app = FastAPI( + title="Gotcha! AI Server", + description="AI Server", + docs_url="/docs", + openapi_url="/openapi.json", + redoc_url="/redoc" +) + +app.include_router(image_router, prefix='/api/v1') + +app.include_router(chat_router, prefix='/api/v1') + + +app.include_router(lulu_router, prefix='/api/v1') \ No newline at end of file diff --git a/train/img.png b/train/img.png new file mode 100644 index 0000000..3199b7c Binary files /dev/null and b/train/img.png differ diff --git a/train/readme.md b/train/readme.md new file mode 100644 index 0000000..6ea509a --- /dev/null +++ b/train/readme.md @@ -0,0 +1,22 @@ +# Quickdraw Classifier + +## ๊ฐœ์š” +EfficientNet-B0 ๋ชจ๋ธ์„ Google์˜ QuickDraw ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํŒŒ์ธํŠœ๋‹ํ•˜์—ฌ ์†์œผ๋กœ ๊ทธ๋ฆฐ ์Šค์ผ€์น˜๋ฅผ ๋ถ„๋ฅ˜ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. + +## ๋ฐ์ดํ„ฐ์…‹ ์ •๋ณด + +- ๋ฐ์ดํ„ฐ์…‹: Google QuickDraw Dataset +- ์นดํ…Œ๊ณ ๋ฆฌ: 345๊ฐœ ํด๋ž˜์Šค (์‚ฌ๊ณผ, ๊ณ ์–‘์ด, ์ž๋™์ฐจ ๋“ฑ) +- ๋ฐ์ดํ„ฐ ํ˜•ํƒœ: 28x28 ํ”ฝ์…€ ํ‘๋ฐฑ ์ด๋ฏธ์ง€ +- ์ด ๋ฐ์ดํ„ฐ: 345๊ฐœ ํด๋ž˜์Šค * 1000์žฅ + +## ๋ชจ๋ธ ์•„ํ‚คํ…์ฒ˜ + +- ๋ฒ ์ด์Šค ๋ชจ๋ธ: EfficientNet-B0 +- ์ž…๋ ฅ ํฌ๊ธฐ: 224x224 (QuickDraw ์ด๋ฏธ์ง€๋ฅผ ์—…์Šค์ผ€์ผ๋ง) +- ์ถœ๋ ฅ: 345๊ฐœ ํด๋ž˜์Šค ๋ถ„๋ฅ˜ + + +## ํ›ˆ๋ จ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™” + +![img.png](img.png) \ No newline at end of file diff --git a/train/train_efficientnet_b0.ipynb b/train/train_efficientnet_b0.ipynb new file mode 100644 index 0000000..8f50f97 --- /dev/null +++ b/train/train_efficientnet_b0.ipynb @@ -0,0 +1,1458 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "84612cf8", + "metadata": {}, + "outputs": [], + "source": [ + "# all name of the quick drawings\n", + "classes = [\n", + " \"aircraft carrier\",\n", + " \"airplane\",\n", + " \"alarm clock\",\n", + " \"ambulance\",\n", + " \"angel\",\n", + " \"animal migration\",\n", + " \"ant\",\n", + " \"anvil\",\n", + " \"apple\",\n", + " \"arm\",\n", + " \"asparagus\",\n", + " \"axe\",\n", + " \"backpack\",\n", + " \"banana\",\n", + " \"bandage\",\n", + " \"barn\",\n", + " \"baseball bat\",\n", + " \"baseball\",\n", + " \"basket\",\n", + " \"basketball\",\n", + " \"bat\",\n", + " \"bathtub\",\n", + " \"beach\",\n", + " \"bear\",\n", + " \"beard\",\n", + " \"bed\",\n", + " \"bee\",\n", + " \"belt\",\n", + " \"bench\",\n", + " \"bicycle\",\n", + " \"binoculars\",\n", + " \"bird\",\n", + " \"birthday cake\",\n", + " \"blackberry\",\n", + " \"blueberry\",\n", + " \"book\",\n", + " \"boomerang\",\n", + " \"bottlecap\",\n", + " \"bowtie\",\n", + " \"bracelet\",\n", + " \"brain\",\n", + " \"bread\",\n", + " \"bridge\",\n", + " \"broccoli\",\n", + " \"broom\",\n", + " \"bucket\",\n", + " \"bulldozer\",\n", + " \"bus\",\n", + " \"bush\",\n", + " \"butterfly\",\n", + " \"cactus\",\n", + " \"cake\",\n", + " \"calculator\",\n", + " \"calendar\",\n", + " \"camel\",\n", + " \"camera\",\n", + " \"camouflage\",\n", + " \"campfire\",\n", + " \"candle\",\n", + " \"cannon\",\n", + " \"canoe\",\n", + " \"car\",\n", + " \"carrot\",\n", + " \"castle\",\n", + " \"cat\",\n", + " \"ceiling fan\",\n", + " \"cell phone\",\n", + " \"cello\",\n", + " \"chair\",\n", + " \"chandelier\",\n", + " \"church\",\n", + " \"circle\",\n", + " \"clarinet\",\n", + " \"clock\",\n", + " \"cloud\",\n", + " \"coffee cup\",\n", + " \"compass\",\n", + " \"computer\",\n", + " \"cookie\",\n", + " \"cooler\",\n", + " \"couch\",\n", + " \"cow\",\n", + " \"crab\",\n", + " \"crayon\",\n", + " \"crocodile\",\n", + " \"crown\",\n", + " \"cruise ship\",\n", + " \"cup\",\n", + " \"diamond\",\n", + " \"dishwasher\",\n", + " \"diving board\",\n", + " \"dog\",\n", + " \"dolphin\",\n", + " \"donut\",\n", + " \"door\",\n", + " \"dragon\",\n", + " \"dresser\",\n", + " \"drill\",\n", + " \"drums\",\n", + " \"duck\",\n", + " \"dumbbell\",\n", + " \"ear\",\n", + " \"elbow\",\n", + " \"elephant\",\n", + " \"envelope\",\n", + " \"eraser\",\n", + " \"eye\",\n", + " \"eyeglasses\",\n", + " \"face\",\n", + " \"fan\",\n", + " \"feather\",\n", + " \"fence\",\n", + " \"finger\",\n", + " \"fire hydrant\",\n", + " \"fireplace\",\n", + " \"firetruck\",\n", + " \"fish\",\n", + " \"flamingo\",\n", + " \"flashlight\",\n", + " \"flip flops\",\n", + " \"floor lamp\",\n", + " \"flower\",\n", + " \"flying saucer\",\n", + " \"foot\",\n", + " \"fork\",\n", + " \"frog\",\n", + " \"frying pan\",\n", + " \"garden hose\",\n", + " \"garden\",\n", + " \"giraffe\",\n", + " \"goatee\",\n", + " \"golf club\",\n", + " \"grapes\",\n", + " \"grass\",\n", + " \"guitar\",\n", + " \"hamburger\",\n", + " \"hammer\",\n", + " \"hand\",\n", + " \"harp\",\n", + " \"hat\",\n", + " \"headphones\",\n", + " \"hedgehog\",\n", + " \"helicopter\",\n", + " \"helmet\",\n", + " \"hexagon\",\n", + " \"hockey puck\",\n", + " \"hockey stick\",\n", + " \"horse\",\n", + " \"hospital\",\n", + " \"hot air balloon\",\n", + " \"hot dog\",\n", + " \"hot tub\",\n", + " \"hourglass\",\n", + " \"house plant\",\n", + " \"house\",\n", + " \"hurricane\",\n", + " \"ice cream\",\n", + " \"jacket\",\n", + " \"jail\",\n", + " \"kangaroo\",\n", + " \"key\",\n", + " \"keyboard\",\n", + " \"knee\",\n", + " \"knife\",\n", + " \"ladder\",\n", + " \"lantern\",\n", + " \"laptop\",\n", + " \"leaf\",\n", + " \"leg\",\n", + " \"light bulb\",\n", + " \"lighter\",\n", + " \"lighthouse\",\n", + " \"lightning\",\n", + " \"line\",\n", + " \"lion\",\n", + " \"lipstick\",\n", + " \"lobster\",\n", + " \"lollipop\",\n", + " \"mailbox\",\n", + " \"map\",\n", + " \"marker\",\n", + " \"matches\",\n", + " \"megaphone\",\n", + " \"mermaid\",\n", + " \"microphone\",\n", + " \"microwave\",\n", + " \"monkey\",\n", + " \"moon\",\n", + " \"mosquito\",\n", + " \"motorbike\",\n", + " \"mountain\",\n", + " \"mouse\",\n", + " \"moustache\",\n", + " \"mouth\",\n", + " \"mug\",\n", + " \"mushroom\",\n", + " \"nail\",\n", + " \"necklace\",\n", + " \"nose\",\n", + " \"ocean\",\n", + " \"octagon\",\n", + " \"octopus\",\n", + " \"onion\",\n", + " \"oven\",\n", + " \"owl\",\n", + " \"paint can\",\n", + " \"paintbrush\",\n", + " \"palm tree\",\n", + " \"panda\",\n", + " \"pants\",\n", + " \"paper clip\",\n", + " \"parachute\",\n", + " \"parrot\",\n", + " \"passport\",\n", + " \"peanut\",\n", + " \"pear\",\n", + " \"peas\",\n", + " \"pencil\",\n", + " \"penguin\",\n", + " \"piano\",\n", + " \"pickup truck\",\n", + " \"picture frame\",\n", + " \"pig\",\n", + " \"pillow\",\n", + " \"pineapple\",\n", + " \"pizza\",\n", + " \"pliers\",\n", + " \"police car\",\n", + " \"pond\",\n", + " \"pool\",\n", + " \"popsicle\",\n", + " \"postcard\",\n", + " \"potato\",\n", + " \"power outlet\",\n", + " \"purse\",\n", + " \"rabbit\",\n", + " \"raccoon\",\n", + " \"radio\",\n", + " \"rain\",\n", + " \"rainbow\",\n", + " \"rake\",\n", + " \"remote control\",\n", + " \"rhinoceros\",\n", + " \"rifle\",\n", + " \"river\",\n", + " \"roller coaster\",\n", + " \"rollerskates\",\n", + " \"sailboat\",\n", + " \"sandwich\",\n", + " \"saw\",\n", + " \"saxophone\",\n", + " \"school bus\",\n", + " \"scissors\",\n", + " \"scorpion\",\n", + " \"screwdriver\",\n", + " \"sea turtle\",\n", + " \"see saw\",\n", + " \"shark\",\n", + " \"sheep\",\n", + " \"shoe\",\n", + " \"shorts\",\n", + " \"shovel\",\n", + " \"sink\",\n", + " \"skateboard\",\n", + " \"skull\",\n", + " \"skyscraper\",\n", + " \"sleeping bag\",\n", + " \"smiley face\",\n", + " \"snail\",\n", + " \"snake\",\n", + " \"snorkel\",\n", + " \"snowflake\",\n", + " \"snowman\",\n", + " \"soccer ball\",\n", + " \"sock\",\n", + " \"speedboat\",\n", + " \"spider\",\n", + " \"spoon\",\n", + " \"spreadsheet\",\n", + " \"square\",\n", + " \"squiggle\",\n", + " \"squirrel\",\n", + " \"stairs\",\n", + " \"star\",\n", + " \"steak\",\n", + " \"stereo\",\n", + " \"stethoscope\",\n", + " \"stitches\",\n", + " \"stop sign\",\n", + " \"stove\",\n", + " \"strawberry\",\n", + " \"streetlight\",\n", + " \"string bean\",\n", + " \"submarine\",\n", + " \"suitcase\",\n", + " \"sun\",\n", + " \"swan\",\n", + " \"sweater\",\n", + " \"swing set\",\n", + " \"sword\",\n", + " \"syringe\",\n", + " \"t-shirt\",\n", + " \"table\",\n", + " \"teapot\",\n", + " \"teddy-bear\",\n", + " \"telephone\",\n", + " \"television\",\n", + " \"tennis racquet\",\n", + " \"tent\",\n", + " \"The Eiffel Tower\",\n", + " \"The Great Wall of China\",\n", + " \"The Mona Lisa\",\n", + " \"tiger\",\n", + " \"toaster\",\n", + " \"toe\",\n", + " \"toilet\",\n", + " \"tooth\",\n", + " \"toothbrush\",\n", + " \"toothpaste\",\n", + " \"tornado\",\n", + " \"tractor\",\n", + " \"traffic light\",\n", + " \"train\",\n", + " \"tree\",\n", + " \"triangle\",\n", + " \"trombone\",\n", + " \"truck\",\n", + " \"trumpet\",\n", + " \"umbrella\",\n", + " \"underwear\",\n", + " \"van\",\n", + " \"vase\",\n", + " \"violin\",\n", + " \"washing machine\",\n", + " \"watermelon\",\n", + " \"waterslide\",\n", + " \"whale\",\n", + " \"wheel\",\n", + " \"windmill\",\n", + " \"wine bottle\",\n", + " \"wine glass\",\n", + " \"wristwatch\",\n", + " \"yoga\",\n", + " \"zebra\",\n", + " \"zigzag\",\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "26aa66aa", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "from torch.utils.data import DataLoader\n", + "from torchvision import datasets, transforms, models\n", + "from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import time\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2a58206e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "์‚ฌ์šฉ ์žฅ์น˜: cuda:0\n" + ] + } + ], + "source": [ + "# ์žฅ์น˜ ์„ค์ •\n", + "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", + "print(f\"์‚ฌ์šฉ ์žฅ์น˜: {device}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "39cbc6a7", + "metadata": {}, + "outputs": [], + "source": [ + "# ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ\n", + "data_dir = \"../../../../Desktop/BE_thief/train/quickdraw_dataset\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d220311a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'12.8'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "torch.version.cuda" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d260f5dc", + "metadata": {}, + "outputs": [], + "source": [ + "# ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ๋ฐ ์ฆ๊ฐ•\n", + "data_transforms = {\n", + " 'train': transforms.Compose([\n", + " transforms.Resize(256),\n", + " transforms.CenterCrop(224),\n", + " transforms.RandomHorizontalFlip(),\n", + " transforms.RandomRotation(10),\n", + " transforms.ToTensor(),\n", + " transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n", + " ]),\n", + " 'val': transforms.Compose([\n", + " transforms.Resize(256),\n", + " transforms.CenterCrop(224),\n", + " transforms.ToTensor(),\n", + " transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n", + " ]),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e9a7a418", + "metadata": {}, + "outputs": [], + "source": [ + "# ๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ\n", + "def load_datasets():\n", + " # train๊ณผ validation ํด๋”๊ฐ€ ๋ฏธ๋ฆฌ ๋‚˜๋ˆ„์–ด์ ธ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •\n", + " # ์—†๋‹ค๋ฉด ์•„๋ž˜ ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ์…‹์„ ๋ถ„ํ• ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค\n", + " \n", + " # ๋ฐ์ดํ„ฐ์…‹์ด ์ด๋ฏธ train/val๋กœ ๋‚˜๋‰˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ\n", + " if os.path.isdir(os.path.join(data_dir, 'train')) and os.path.isdir(os.path.join(data_dir, 'val')):\n", + " image_datasets = {\n", + " 'train': datasets.ImageFolder(os.path.join(data_dir, 'train'), data_transforms['train']),\n", + " 'val': datasets.ImageFolder(os.path.join(data_dir, 'val'), data_transforms['val'])\n", + " }\n", + " else:\n", + " # ๋ฐ์ดํ„ฐ์…‹์ด ๋‚˜๋‰˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ, ์ „์ฒด ๋ฐ์ดํ„ฐ์…‹์„ ๋กœ๋“œํ•˜๊ณ  ๋ถ„ํ• \n", + " full_dataset = datasets.ImageFolder(data_dir, data_transforms['train'])\n", + " \n", + " # ํด๋ž˜์Šค ์ด๋ฆ„๊ณผ ์ธ๋ฑ์Šค ๊ฐ€์ ธ์˜ค๊ธฐ\n", + " class_names = full_dataset.classes\n", + " print(f\"ํด๋ž˜์Šค ๊ฐœ์ˆ˜: {len(class_names)}\")\n", + " print(f\"ํด๋ž˜์Šค ๋ชฉ๋ก: {class_names}\")\n", + " \n", + " # ๋ฐ์ดํ„ฐ์…‹ ๋ถ„ํ•  (80% ํ›ˆ๋ จ, 20% ๊ฒ€์ฆ)\n", + " train_size = int(0.8 * len(full_dataset))\n", + " val_size = len(full_dataset) - train_size\n", + " train_dataset, val_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size])\n", + " \n", + " # val ๋ฐ์ดํ„ฐ์…‹์€ ๋‹ค๋ฅธ ๋ณ€ํ™˜ ์ ์šฉ\n", + " val_dataset.dataset.transform = data_transforms['val']\n", + " \n", + " image_datasets = {\n", + " 'train': train_dataset,\n", + " 'val': val_dataset\n", + " }\n", + " \n", + " # ๋ฐ์ดํ„ฐ๋กœ๋” ์ƒ์„ฑ\n", + " dataloaders = {\n", + " 'train': DataLoader(image_datasets['train'], batch_size=32, shuffle=True, num_workers=4),\n", + " 'val': DataLoader(image_datasets['val'], batch_size=32, shuffle=False, num_workers=4)\n", + " }\n", + " \n", + " dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}\n", + " class_names = image_datasets['train'].dataset.classes if hasattr(image_datasets['train'], 'dataset') else image_datasets['train'].classes\n", + " \n", + " print(f\"๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ: train={dataset_sizes['train']}, val={dataset_sizes['val']}\")\n", + " print(f\"ํด๋ž˜์Šค ๊ฐœ์ˆ˜: {len(class_names)}\")\n", + " \n", + " return dataloaders, dataset_sizes, class_names" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d8efa701", + "metadata": {}, + "outputs": [], + "source": [ + "# EfficientNet-B0 ๋ชจ๋ธ ๋กœ๋“œ ๋ฐ ์ˆ˜์ •\n", + "def setup_model(num_classes):\n", + " # ์‚ฌ์ „ ํ›ˆ๋ จ๋œ EfficientNet-B0 ๋ชจ๋ธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ\n", + " model = efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1)\n", + " \n", + " # ๋งˆ์ง€๋ง‰ ๋ถ„๋ฅ˜๊ธฐ ๋ ˆ์ด์–ด ์ˆ˜์ • (QuickDraw ํด๋ž˜์Šค ์ˆ˜์— ๋งž๊ฒŒ)\n", + " num_ftrs = model.classifier[1].in_features\n", + " model.classifier[1] = nn.Linear(num_ftrs, num_classes)\n", + " \n", + " # ๋ชจ๋ธ์„ ์ง€์ •๋œ ์žฅ์น˜(GPU/CPU)๋กœ ์ด๋™\n", + " model = model.to(device)\n", + " \n", + " return model" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9442775c", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# ํ•™์Šต ํ•จ์ˆ˜\n", + "def train_model(model, dataloaders, dataset_sizes, criterion, optimizer, scheduler, num_epochs=25):\n", + " since = time.time()\n", + " \n", + " best_model_wts = model.state_dict()\n", + " best_acc = 0.0\n", + " \n", + " # ํ•™์Šต ๊ณผ์ • ๊ธฐ๋ก\n", + " history = {\n", + " 'train_loss': [],\n", + " 'val_loss': [],\n", + " 'train_acc': [],\n", + " 'val_acc': []\n", + " }\n", + " \n", + " for epoch in range(num_epochs):\n", + " print(f'Epoch {epoch+1}/{num_epochs}')\n", + " print('-' * 10)\n", + " \n", + " # ๊ฐ ์—ํฌํฌ๋Š” ํ•™์Šต๊ณผ ๊ฒ€์ฆ ๋‹จ๊ณ„๊ฐ€ ์žˆ์Œ\n", + " for phase in ['train', 'val']:\n", + " if phase == 'train':\n", + " model.train() # ํ•™์Šต ๋ชจ๋“œ ์„ค์ •\n", + " else:\n", + " model.eval() # ํ‰๊ฐ€ ๋ชจ๋“œ ์„ค์ •\n", + " \n", + " running_loss = 0.0\n", + " running_corrects = 0\n", + " \n", + " # ๋ฐ์ดํ„ฐ ๋ฐ˜๋ณต\n", + " for inputs, labels in tqdm(dataloaders[phase]):\n", + " inputs = inputs.to(device)\n", + " labels = labels.to(device)\n", + " \n", + " # ํŒŒ๋ผ๋ฏธํ„ฐ ๊ทธ๋ž˜๋””์–ธํŠธ ์ดˆ๊ธฐํ™”\n", + " optimizer.zero_grad()\n", + " \n", + " # ์ˆœ์ „ํŒŒ\n", + " with torch.set_grad_enabled(phase == 'train'):\n", + " outputs = model(inputs)\n", + " _, preds = torch.max(outputs, 1)\n", + " loss = criterion(outputs, labels)\n", + " \n", + " # ํ•™์Šต ๋‹จ๊ณ„์ผ ๊ฒฝ์šฐ ์—ญ์ „ํŒŒ + ์ตœ์ ํ™”\n", + " if phase == 'train':\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # ํ†ต๊ณ„\n", + " running_loss += loss.item() * inputs.size(0)\n", + " running_corrects += torch.sum(preds == labels.data)\n", + " \n", + " if phase == 'train' and scheduler is not None:\n", + " scheduler.step()\n", + " \n", + " epoch_loss = running_loss / dataset_sizes[phase]\n", + " epoch_acc = running_corrects.double() / dataset_sizes[phase]\n", + " \n", + " # ๊ธฐ๋ก ์ €์žฅ\n", + " if phase == 'train':\n", + " history['train_loss'].append(epoch_loss)\n", + " history['train_acc'].append(epoch_acc.item())\n", + " else:\n", + " history['val_loss'].append(epoch_loss)\n", + " history['val_acc'].append(epoch_acc.item())\n", + " \n", + " print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')\n", + " \n", + " # ๋ชจ๋ธ์„ ๋ณต์‚ฌ (์ตœ๊ณ ์˜ ๊ฒ€์ฆ ์ •ํ™•๋„๋ฅผ ๊ธฐ๋กํ•œ ๊ฒฝ์šฐ)\n", + " if phase == 'val' and epoch_acc > best_acc:\n", + " best_acc = epoch_acc\n", + " best_model_wts = model.state_dict()\n", + " \n", + " print()\n", + " \n", + " time_elapsed = time.time() - since\n", + " print(f'ํ•™์Šต ์™„๋ฃŒ: {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')\n", + " print(f'์ตœ๊ณ  ๊ฒ€์ฆ ์ •ํ™•๋„: {best_acc:.4f}')\n", + " \n", + " # ๊ฐ€์žฅ ์ข‹์€ ๋ชจ๋ธ ๊ฐ€์ค‘์น˜ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ\n", + " model.load_state_dict(best_model_wts)\n", + " return model, history" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f6339dda", + "metadata": {}, + "outputs": [], + "source": [ + "# ํ•™์Šต ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”\n", + "def plot_training_history(history):\n", + " plt.figure(figsize=(12, 4))\n", + " \n", + " plt.subplot(1, 2, 1)\n", + " plt.plot(history['train_loss'], label='Train Loss')\n", + " plt.plot(history['val_loss'], label='Validation Loss')\n", + " plt.xlabel('Epoch')\n", + " plt.ylabel('Loss')\n", + " plt.legend()\n", + " plt.title('Training and Validation Loss')\n", + " \n", + " plt.subplot(1, 2, 2)\n", + " plt.plot(history['train_acc'], label='Train Accuracy')\n", + " plt.plot(history['val_acc'], label='Validation Accuracy')\n", + " plt.xlabel('Epoch')\n", + " plt.ylabel('Accuracy')\n", + " plt.legend()\n", + " plt.title('Training and Validation Accuracy')\n", + " \n", + " plt.tight_layout()\n", + " plt.savefig('training_history.png')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2c7e0547", + "metadata": {}, + "outputs": [], + "source": [ + "# ๋ชจ๋ธ ์ €์žฅ ํ•จ์ˆ˜\n", + "def save_model(model, class_names, filename='efficientnet_b0_quickdraw.pth'):\n", + " model_info = {\n", + " 'model_state_dict': model.state_dict(),\n", + " 'class_names': class_names,\n", + " 'model_name': 'efficientnet_b0'\n", + " }\n", + " torch.save(model_info, filename)\n", + " print(f\"๋ชจ๋ธ์ด {filename}์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d985a0f8", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# ํ…Œ์ŠคํŠธ ์„ธํŠธ์—์„œ ๋ชจ๋ธ ํ‰๊ฐ€\n", + "def evaluate_model(model, test_loader):\n", + " model.eval()\n", + " correct = 0\n", + " total = 0\n", + " \n", + " with torch.no_grad():\n", + " for inputs, labels in tqdm(test_loader):\n", + " inputs = inputs.to(device)\n", + " labels = labels.to(device)\n", + " \n", + " outputs = model(inputs)\n", + " _, predicted = torch.max(outputs.data, 1)\n", + " \n", + " total += labels.size(0)\n", + " correct += (predicted == labels).sum().item()\n", + " \n", + " accuracy = 100 * correct / total\n", + " print(f'ํ…Œ์ŠคํŠธ ์ •ํ™•๋„: {accuracy:.2f}%')\n", + " return accuracy" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "bda50278", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def main():\n", + " # ๋ฐ์ดํ„ฐ ๋กœ๋“œ\n", + " dataloaders, dataset_sizes, class_names = load_datasets()\n", + " \n", + " # ๋ชจ๋ธ ์„ค์ •\n", + " num_classes = len(class_names)\n", + " model = setup_model(num_classes)\n", + " \n", + " # ์†์‹ค ํ•จ์ˆ˜์™€ ์˜ตํ‹ฐ๋งˆ์ด์ € ์„ค์ •\n", + " criterion = nn.CrossEntropyLoss()\n", + " optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)\n", + " \n", + " # ํ•™์Šต๋ฅ  ์Šค์ผ€์ค„๋Ÿฌ (10 ์—ํฌํฌ๋งˆ๋‹ค ํ•™์Šต๋ฅ ์„ 0.1๋ฐฐ๋กœ ๊ฐ์†Œ)\n", + " scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)\n", + " \n", + " # ๋ชจ๋ธ ํ•™์Šต\n", + " print(\"๋ชจ๋ธ ํ•™์Šต ์‹œ์ž‘...\")\n", + " model, history = train_model(model, dataloaders, dataset_sizes, criterion, optimizer, scheduler, num_epochs=20)\n", + " \n", + " # ํ•™์Šต ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”\n", + " plot_training_history(history)\n", + " \n", + " # ๊ฒ€์ฆ ์„ธํŠธ์—์„œ ํ‰๊ฐ€\n", + " print(\"๊ฒ€์ฆ ์„ธํŠธ์—์„œ ๋ชจ๋ธ ํ‰๊ฐ€ ์ค‘...\")\n", + " evaluate_model(model, dataloaders['val'])\n", + " \n", + " # ๋ชจ๋ธ ์ €์žฅ\n", + " save_model(model, class_names)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "6ca361f7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ํด๋ž˜์Šค ๊ฐœ์ˆ˜: 345\n", + "ํด๋ž˜์Šค ๋ชฉ๋ก: ['The_Eiffel_Tower', 'The_Great_Wall_of_China', 'The_Mona_Lisa', 'aircraft_carrier', 'airplane', 'alarm_clock', 'ambulance', 'angel', 'animal_migration', 'ant', 'anvil', 'apple', 'arm', 'asparagus', 'axe', 'backpack', 'banana', 'bandage', 'barn', 'baseball', 'baseball_bat', 'basket', 'basketball', 'bat', 'bathtub', 'beach', 'bear', 'beard', 'bed', 'bee', 'belt', 'bench', 'bicycle', 'binoculars', 'bird', 'birthday_cake', 'blackberry', 'blueberry', 'book', 'boomerang', 'bottlecap', 'bowtie', 'bracelet', 'brain', 'bread', 'bridge', 'broccoli', 'broom', 'bucket', 'bulldozer', 'bus', 'bush', 'butterfly', 'cactus', 'cake', 'calculator', 'calendar', 'camel', 'camera', 'camouflage', 'campfire', 'candle', 'cannon', 'canoe', 'car', 'carrot', 'castle', 'cat', 'ceiling_fan', 'cell_phone', 'cello', 'chair', 'chandelier', 'church', 'circle', 'clarinet', 'clock', 'cloud', 'coffee_cup', 'compass', 'computer', 'cookie', 'cooler', 'couch', 'cow', 'crab', 'crayon', 'crocodile', 'crown', 'cruise_ship', 'cup', 'diamond', 'dishwasher', 'diving_board', 'dog', 'dolphin', 'donut', 'door', 'dragon', 'dresser', 'drill', 'drums', 'duck', 'dumbbell', 'ear', 'elbow', 'elephant', 'envelope', 'eraser', 'eye', 'eyeglasses', 'face', 'fan', 'feather', 'fence', 'finger', 'fire_hydrant', 'fireplace', 'firetruck', 'fish', 'flamingo', 'flashlight', 'flip_flops', 'floor_lamp', 'flower', 'flying_saucer', 'foot', 'fork', 'frog', 'frying_pan', 'garden', 'garden_hose', 'giraffe', 'goatee', 'golf_club', 'grapes', 'grass', 'guitar', 'hamburger', 'hammer', 'hand', 'harp', 'hat', 'headphones', 'hedgehog', 'helicopter', 'helmet', 'hexagon', 'hockey_puck', 'hockey_stick', 'horse', 'hospital', 'hot_air_balloon', 'hot_dog', 'hot_tub', 'hourglass', 'house', 'house_plant', 'hurricane', 'ice_cream', 'jacket', 'jail', 'kangaroo', 'key', 'keyboard', 'knee', 'knife', 'ladder', 'lantern', 'laptop', 'leaf', 'leg', 'light_bulb', 'lighter', 'lighthouse', 'lightning', 'line', 'lion', 'lipstick', 'lobster', 'lollipop', 'mailbox', 'map', 'marker', 'matches', 'megaphone', 'mermaid', 'microphone', 'microwave', 'monkey', 'moon', 'mosquito', 'motorbike', 'mountain', 'mouse', 'moustache', 'mouth', 'mug', 'mushroom', 'nail', 'necklace', 'nose', 'ocean', 'octagon', 'octopus', 'onion', 'oven', 'owl', 'paint_can', 'paintbrush', 'palm_tree', 'panda', 'pants', 'paper_clip', 'parachute', 'parrot', 'passport', 'peanut', 'pear', 'peas', 'pencil', 'penguin', 'piano', 'pickup_truck', 'picture_frame', 'pig', 'pillow', 'pineapple', 'pizza', 'pliers', 'police_car', 'pond', 'pool', 'popsicle', 'postcard', 'potato', 'power_outlet', 'purse', 'rabbit', 'raccoon', 'radio', 'rain', 'rainbow', 'rake', 'remote_control', 'rhinoceros', 'rifle', 'river', 'roller_coaster', 'rollerskates', 'sailboat', 'sandwich', 'saw', 'saxophone', 'school_bus', 'scissors', 'scorpion', 'screwdriver', 'sea_turtle', 'see_saw', 'shark', 'sheep', 'shoe', 'shorts', 'shovel', 'sink', 'skateboard', 'skull', 'skyscraper', 'sleeping_bag', 'smiley_face', 'snail', 'snake', 'snorkel', 'snowflake', 'snowman', 'soccer_ball', 'sock', 'speedboat', 'spider', 'spoon', 'spreadsheet', 'square', 'squiggle', 'squirrel', 'stairs', 'star', 'steak', 'stereo', 'stethoscope', 'stitches', 'stop_sign', 'stove', 'strawberry', 'streetlight', 'string_bean', 'submarine', 'suitcase', 'sun', 'swan', 'sweater', 'swing_set', 'sword', 'syringe', 't-shirt', 'table', 'teapot', 'teddy-bear', 'telephone', 'television', 'tennis_racquet', 'tent', 'tiger', 'toaster', 'toe', 'toilet', 'tooth', 'toothbrush', 'toothpaste', 'tornado', 'tractor', 'traffic_light', 'train', 'tree', 'triangle', 'trombone', 'truck', 'trumpet', 'umbrella', 'underwear', 'van', 'vase', 'violin', 'washing_machine', 'watermelon', 'waterslide', 'whale', 'wheel', 'windmill', 'wine_bottle', 'wine_glass', 'wristwatch', 'yoga', 'zebra', 'zigzag']\n", + "๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ: train=828000, val=207000\n", + "ํด๋ž˜์Šค ๊ฐœ์ˆ˜: 345\n", + "๋ชจ๋ธ ํ•™์Šต ์‹œ์ž‘...\n", + "Epoch 1/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [48:52<00:00, 8.82it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.6663 Acc: 0.5972\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:01<00:00, 26.83it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.3146 Acc: 0.6749\n", + "\n", + "Epoch 2/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [48:57<00:00, 8.81it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.2951 Acc: 0.6768\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:02<00:00, 26.71it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.2120 Acc: 0.6995\n", + "\n", + "Epoch 3/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:15<00:00, 9.13it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.1890 Acc: 0.6994\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:51<00:00, 27.90it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.5957 Acc: 0.6075\n", + "\n", + "Epoch 4/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:37<00:00, 9.05it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.1188 Acc: 0.7151\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:54<00:00, 27.54it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1327 Acc: 0.7182\n", + "\n", + "Epoch 5/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:20<00:00, 9.11it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.0648 Acc: 0.7264\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:51<00:00, 27.89it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.5308 Acc: 0.6226\n", + "\n", + "Epoch 6/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [50:11<00:00, 8.59it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 1.0204 Acc: 0.7364\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [05:03<00:00, 21.31it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1138 Acc: 0.7249\n", + "\n", + "Epoch 7/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [49:38<00:00, 8.69it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.9813 Acc: 0.7443\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:52<00:00, 22.09it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0951 Acc: 0.7286\n", + "\n", + "Epoch 8/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [48:56<00:00, 8.81it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.9465 Acc: 0.7527\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:50<00:00, 22.27it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0990 Acc: 0.7294\n", + "\n", + "Epoch 9/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [49:51<00:00, 8.65it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.9154 Acc: 0.7590\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:52<00:00, 22.09it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0968 Acc: 0.7317\n", + "\n", + "Epoch 10/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [50:14<00:00, 8.58it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.8875 Acc: 0.7650\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:57<00:00, 21.73it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0962 Acc: 0.7316\n", + "\n", + "Epoch 11/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [50:08<00:00, 8.60it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.7159 Acc: 0.8086\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [04:57<00:00, 21.78it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0554 Acc: 0.7470\n", + "\n", + "Epoch 12/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [49:23<00:00, 8.73it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.6641 Acc: 0.8210\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.86it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0701 Acc: 0.7472\n", + "\n", + "Epoch 13/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:10<00:00, 9.14it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.6381 Acc: 0.8277\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.84it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0737 Acc: 0.7461\n", + "\n", + "Epoch 14/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:15<00:00, 9.13it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.6182 Acc: 0.8324\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.88it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0880 Acc: 0.7455\n", + "\n", + "Epoch 15/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:08<00:00, 9.15it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.6005 Acc: 0.8362\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.87it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.0961 Acc: 0.7443\n", + "\n", + "Epoch 16/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:09<00:00, 9.15it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.5848 Acc: 0.8407\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.85it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1069 Acc: 0.7441\n", + "\n", + "Epoch 17/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:13<00:00, 9.13it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.5715 Acc: 0.8434\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:53<00:00, 27.72it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1176 Acc: 0.7431\n", + "\n", + "Epoch 18/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:45<00:00, 9.03it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.5578 Acc: 0.8467\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.82it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1342 Acc: 0.7419\n", + "\n", + "Epoch 19/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:13<00:00, 9.13it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.5459 Acc: 0.8491\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.82it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1417 Acc: 0.7410\n", + "\n", + "Epoch 20/20\n", + "----------\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 25875/25875 [47:49<00:00, 9.02it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.5342 Acc: 0.8521\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:56<00:00, 27.36it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 1.1481 Acc: 0.7401\n", + "\n", + "ํ•™์Šต ์™„๋ฃŒ: 1051m 27s\n", + "์ตœ๊ณ  ๊ฒ€์ฆ ์ •ํ™•๋„: 0.7472\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAAxWNJREFUeJzs3Qd4VNXWBuAvvZKEVFJJ6L0XqYKCqIiKvaLYfvViw967WNGr164Ioti7WECk995LIEB6hXTS8z9rn5xk0kibzJlJvvd5DtNndiYzZJ911lrbrry8vBxEREREREREREQWZG/JFyMiIiIiIiIiIhIMShERERERERERkcUxKEVERERERERERBbHoBQREREREREREVkcg1JERERERERERGRxDEoREREREREREZHFMShFREREREREREQWx6AUERERERERERFZHINSRERERERERERkcQxKEVm5G2+8EZGRkc167DPPPAM7Ozu0ZceOHVM/4/z58y3+2vK68h7rZAxynYypIfI7ld+ttXxWiIiIrA3nQKfHOVAVzoGIbBeDUkTNJH94G7OtWLHC6KG2e3fffbf6XRw+fLje+zz++OPqPrt27YI1S0xMVJPAHTt2wNomxa+//rrRQyEiIgvgHMh2cA5kOfv371fvo6urKzIzM40eDpHNcDR6AES2auHChdUuf/7551i6dGmt63v37t2i1/n4449RVlbWrMc+8cQTeOSRR9DeXXvttXjnnXewaNEiPPXUU3Xe56uvvkL//v0xYMCAZr/O9ddfj6uuugouLi5ozQnZs88+q44GDho0yGyfFSIiosbiHMh2cA5kOV988QU6deqEkydP4vvvv8ctt9xi6HiIbAWDUkTNdN1111W7vGHDBjUhq3l9Tfn5+XB3d2/06zg5OTV7jI6Ojmpr70aOHIlu3bqpSVddE7L169fj6NGjePnll1v0Og4ODmozSks+K0RERI3FOZDt4BzIMsrLy1Xg75prrlHv55dffmm1Qam8vDx4eHgYPQyiSizfI2pFEyZMQL9+/bB161aMHz9eTcQee+wxddsvv/yCqVOnIiQkRB1V6tq1K55//nmUlpaetkbetFTqo48+Uo+Txw8fPhybN29usJ+CXJ41axZ+/vlnNTZ5bN++ffHXX3/VGr+k3Q8bNkylIcvrfPjhh43u0bB69WpcfvnliIiIUK8R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XnnlFfntb38rgUDfNN2GDRtm3iz0m6t33323XdnuYPHqq6+an4Vm7oP5fKMlrXdy9913m+Kk+gHphNfLlClTPNe1MKn2ZcCAAeabvvHjx4sT6B9E+g3dxYrrBss5B/zBSfGU4v2CeMrpiKmIqZxwzjuDkVIdkJycLGFhYWYInjfdTk9Pb/UxentH9re7WbNmyT//+U/ZtGmT9O3bt0OPjYiIkKuvvloOHjwogUqH2Q4aNKjNPgTb+VZaXHP9+vXyy1/+0nHn233eOnJOO/M+YffgSX8HtDDrhb7R68zrJVDoEGo9r231I5jOufr4449NEdaOvuaD6Zyj+zk9pnJ6POXEmMrJ8ZQipiKmUsRUzjvn7UFSqgMiIyNl5MiRZvihmw6p1W3vbzS86e3e+yt9I2prf7vSjL4GUCtWrJCNGzfKpZde2uHn0KGYe/bsMcvABiqd43/o0KE2+xAs59vb66+/buaB33777Y473/p7rh+A3ue0vLzcrBjT1jntzPuEnYMnnduuQXTv3r27/PUSKHTKhNY/aKsfwXLOvb/J1/7oqjJOPefofk6NqYinnBtTOTmeUsRUxFSKmMp557xdLlIIHed5++23zSoRb7zxhvX1119bDz/8sJWUlGQVFBSY+++77z7rqaee8uz/ySefWOHh4daf/vQn65tvvjFV9SMiIqw9e/ZYgWT69OlmJZDNmzdbJ06c8LQzZ8549jm/7wsWLLDWrVtnHTp0yNq5c6c1ZcoUKzo62tq7d68VKH7961+bPh85csScywkTJljJyclmtZxgPt/eq11ccskl1ty5c1vcFyznu6Kiwvriiy9M07fEF154wVx3r4jy+9//3rzGV65cae3evdusnnHppZda1dXVnufQ1TRefPHFdr9P2L3fdXV11h133GH17dvX2rVrV7PXfG1tbZv9vtjrJRD6rvc9+eST1rZt20w/1q9fb11zzTXWwIEDrZqamqA9525lZWVWbGys9fLLL7f6HIF6zmFPToypnBpPOT2mckI8pYipiKmIqYipOoOkVCfoL49+sERGRpolK7dv3+657/rrrzdLX3p79913rUGDBpn9hw4daq1evdoKNPpia63psrVt9f2xxx7z/JzS0tKsSZMmWZ9//rkVSO655x4rIyPD9KFPnz5m++DBg0F/vt00KNLznJub2+K+YDnfmzZtavV32903XcL42WefNX3SD8jx48e3+HlkZ2ebYLm97xN277d+GLb1mtfHtdXvi71eAqHv+ofhLbfcYqWkpJg/frSPDz30UItAKNjOudsrr7xixcTEmGW6WxOo5xz25bSYyqnxlNNjKifEU4qYipiKmIqYqjNC9J/2jakCAAAAAAAAugY1pQAAAAAAAOBzJKUAAAAAAADgcySlAAAAAAAA4HMkpQAAAAAAAOBzJKUAAAAAAADgcySlAAAAAAAA4HMkpQAAAAAAAOBzJKUAAAAAAADgcySlAKATQkJC5IMPPvD3YQAAAAQ0YirA2UhKAQg4DzzwgAlgzm+33nqrvw8NAAAgYBBTAfC3cH8fAAB0hgZLr7/+erPboqKi/HY8AAAAgYiYCoA/MVIKQEDSYCk9Pb1Z69mzp7lPv+F7+eWX5bbbbpOYmBjp37+/vP/++80ev2fPHrnpppvM/b1795aHH35YKisrm+3z2muvydChQ83/lZGRIbNmzWp2f3FxsfzoRz+S2NhYGThwoKxatcoHPQcAAOg6xFQA/ImkFICg9Oyzz8pdd90lX375pUydOlWmTJki33zzjbmvqqpKJk6caAKuzz77TN577z1Zv359swBJA7CZM2eawEqDLQ2OLrvssmb/x4IFC+Tuu++W3bt3y6RJk8z/U1JS4vO+AgAAdBdiKgDdygKAADNt2jQrLCzMiouLa9Z+97vfmfv1re2RRx5p9picnBxr+vTp5vqyZcusnj17WpWVlZ77V69ebYWGhloFBQVmOzMz03rmmWfaPAb9P37zm994tvW59LZ//etfXd5fAACA7kBMBcDfqCkFICDdeOON5ps3b7169fJcHzt2bLP7dHvXrl3mun67N3z4cImLi/PcP27cOHG5XJKbm2uGqufn58v48eMveAzDhg3zXNfnSkhIkJMnT/7XfQMAAPAVYioA/kRSCkBA0oDl/KHfXUVrIrRHREREs20NvDQIAwAACBTEVAD8iZpSAILS9u3bW2wPGTLEXNdLrYugdRDcPvnkEwkNDZXBgwdLfHy89OvXTzZs2ODz4wYAALATYioA3YmRUgACUm1trRQUFDS7LTw8XJKTk811LbQ5atQo+d73vif/+Mc/ZMeOHfLqq6+a+7R45vz582XatGny3HPPSVFRkcyePVvuu+8+SUtLM/vo7Y888oikpqaaFWcqKipMkKX7AQAABAtiKgD+RFIKQEBau3atWVLYm34jt2/fPs8qLm+//bbMmDHD7PfWW2/JFVdcYe7T5YbXrVsnc+bMkdGjR5ttXVXmhRde8DyXBlc1NTXy5z//WZ588kkTmP3kJz/xcS8BAAC6FzEVAH8K0Wrnfj0CAOhiWodgxYoVMnnyZH8fCgAAQMAipgLQ3agpBQAAAAAAAJ8jKQUAAAAAAACfY/oeAAAAAAAAfI6RUgAAAAAAAPA5klIAAAAAAADwOZJSAAAAAAAA8DmSUgAAAAAAAPA5klIAAAAAAADwOZJSAAAAAAAA8DmSUgAAAAAAAPA5klIAAAAAAADwOZJSAAAAAAAAEF/7PwQw7y7lamzSAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๊ฒ€์ฆ ์„ธํŠธ์—์„œ ๋ชจ๋ธ ํ‰๊ฐ€ ์ค‘...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 6469/6469 [03:52<00:00, 27.84it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ํ…Œ์ŠคํŠธ ์ •ํ™•๋„: 74.01%\n", + "๋ชจ๋ธ์ด efficientnet_b0_quickdraw.pth์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebb560a3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}