diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 44e57aa..8e6ff02 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -47,8 +47,7 @@ jobs: username: ${{ secrets.SERVER_USERNAME }} key: ${{ secrets.SERVER_PRIVATE_KEY }} script: | - cd ~/ai-server - + docker stop ai-server || true docker rm ai-server || true @@ -61,53 +60,4 @@ jobs: --name ai-server \ --env-file .env \ -p 8000:8000 \ - ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest - - notify: - needs: deploy - runs-on: ubuntu-latest - - - - steps: - - name: Slack notification on success - if: success() - uses: slackapi/slack-github-action@v2 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - { - "channel": "${{ secrets.SLACK_CHANNEL_ID }}", - "text": "✅ *배포 성공:* ${{ github.repository }} 저장소의 `${{ github.ref_name }}` 브랜치가 성공적으로 배포되었습니다.", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Status:* `${{ job.status }}`\n*Commit:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>\n*Actor:* ${{ github.actor }}" - } - } - ] - } - - - name: Slack notification on failure - if: failure() - uses: slackapi/slack-github-action@v2 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - { - "channel": "${{ secrets.SLACK_CHANNEL_ID }}", - "text": "❌ *배포 실패:* ${{ github.repository }} 저장소의 `${{ github.ref_name }}` 브랜치 배포가 실패했습니다.", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Status:* `${{ job.status }}`\n*Commit:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>\n*Actor:* ${{ github.actor }}" - } - } - ] - } \ No newline at end of file + ${{ secrets.DOCKER_USERNAME }}/gotcha-ai:latest \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index 601b601..0000000 --- a/config.py +++ /dev/null @@ -1,294 +0,0 @@ - -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/models/classifying_model.pth b/models/classifying_model.pth new file mode 100644 index 0000000..ad01257 Binary files /dev/null and b/models/classifying_model.pth differ diff --git a/requirements.txt b/requirements.txt index 89a7212..0780db4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,16 @@ +fastapi uvicorn python-multipart -fastapi -openai \ No newline at end of file +transformers +httpx +Pillow +--extra-index-url https://download.pytorch.org/whl/cpu +torch +torchvision +pydantic +openai +requests +easyocr +numpy +opencv-python-headless +boto3 \ No newline at end of file diff --git a/src/api/captioning.py b/src/api/captioning.py new file mode 100644 index 0000000..9d4a4e3 --- /dev/null +++ b/src/api/captioning.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, Field + +from src.core.caption import generate_caption + +router = APIRouter( + tags=["Image Captioning"] +) + +class ImageReq(BaseModel): + image_url: str = Field(description="S3 이미지 URL") + +class CaptionRes(BaseModel): + caption: str = Field(description="이미지를 묘사하는 문장") + +@router.post( + "/caption", + summary="이미지 문장 추출 API", + description="S3 이미지 URL을 받아 해당 이미지를 묘사하는 적절한 문장을 반환합니다.", + response_model=CaptionRes, +) +async def caption_image(request: Request, body: ImageReq): + try: + response = await request.app.state.http.get(body.image_url) + response.raise_for_status() + if not response.headers.get("content-type", "").startswith("image/"): + raise HTTPException(415, "지원하지 않는 콘텐츠 유형입니다. 이미지 파일만 허용됩니다.") + caption = generate_caption(response.content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"이미지 처리 중 오류가 발생했습니다: {e}") + + return CaptionRes(caption=caption) \ No newline at end of file diff --git a/src/api/classifying.py b/src/api/classifying.py new file mode 100644 index 0000000..335afd8 --- /dev/null +++ b/src/api/classifying.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, Field +from src.core.classify import classify +router = APIRouter( + tags=["Image Classification"], +) + +class ImageReq(BaseModel): + image_url: str = Field(description="S3 이미지 URL") + +class AiPrediction(BaseModel): + predicted: str = Field(description="예측된 카테고리 (한국어)") + confidence: float = Field(description="신뢰도 점수") + +class ClassifyRes(BaseModel): + filename: str = Field(description="이미지 파일 이름") + result: list[AiPrediction] = Field(description="분류 결과 리스트") + + + + +@router.post( + "/classify", + summary="이미지 분류 API", + description="S3 이미지 URL을 받아 해당 이미지의 분류 결과를 반환합니다.", + response_model=ClassifyRes, +) +async def classify_image(request: Request, body: ImageReq): + try: + response = await request.app.state.http.get(body.image_url) + response.raise_for_status() + if not response.headers.get("content-type", "").startswith("image/"): + raise HTTPException(415, "Unsupported content-type") + predictions = classify(response.content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error processing image: {e}") + + filename = body.image_url.split("/")[-1] + result = [AiPrediction(**pred) for pred in predictions] + return ClassifyRes(filename=filename, result=result) \ No newline at end of file diff --git a/src/api/image_routes.py b/src/api/image_routes.py deleted file mode 100644 index 0e1edb7..0000000 --- a/src/api/image_routes.py +++ /dev/null @@ -1,80 +0,0 @@ -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)}") - - result = [ - {'predicted': '항공모함', 'confidence': 0.85}, - {'predicted': '비행기', 'confidence': 0.10 }, - {'predicted': '커피', 'confidence' : 0.05} - ] - 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 "a black and white drawing of cat" \ No newline at end of file diff --git a/src/api/lulu_routes.py b/src/api/lulu.py similarity index 52% rename from src/api/lulu_routes.py rename to src/api/lulu.py index 27881e8..1c54eb5 100644 --- a/src/api/lulu_routes.py +++ b/src/api/lulu.py @@ -1,68 +1,45 @@ -from typing import List - -from fastapi import APIRouter, Body +from fastapi import APIRouter, Body from pydantic import BaseModel, Field - -from src.chat.lulu import LuLuAI import os -router = APIRouter(prefix = '/lulu', tags = ['LuLu']) +from src.core.lulu import LuLuAI -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +router = APIRouter( + prefix="/lulu", + tags=["LuLu"], +) +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") lulu = LuLuAI(api_key=OPENAI_API_KEY) - +class LuLuStartRes(BaseModel): + game_id: str = Field(..., description="게임 ID") @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 } + return LuLuStartRes(game_id=game_id) + +class LuLuTaskGenerateRes(BaseModel): + keyword: str = Field(..., description="제시된 키워드") + situation: str = Field(..., description="시적인 상황 설명") @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 - - +async def generate_task(game_id: str): + task = await lulu.generate_drawing_task(game_id) + return LuLuTaskGenerateRes(keyword=task["keyword"], situation=task["situation"]) class EvaluationReq(BaseModel): description: str = Field(..., description="그린 그림에 대한 설명") - +# todo: 루루 응답 스키마 관련 리팩토링(core 계층에서부터) @router.post( "/task/{game_id}", summary="그린 그림에 대한 설명을 루루에게 제출하고 평가를 받습니다.", @@ -75,8 +52,8 @@ class EvaluationReq(BaseModel): "score": 20, "feedback": "뜨거운 태양과 모래사장이라... 이게 무슨 뜻이야? 시적 묘사를 제대로 이해하고 있나? 흐름과 장막, 마지막 이야기를 속삭이는 곳, 잃어버린 순간들이 춤추는 곳... 이런 모든 것들이 바다를 묘사하는 것이지. 너의 그림은 바다의 본질을 전혀 담아내지 못했어. 예술적 표현력이나 창의성은 어디에 있는 거야? 너의 그림은 완성도나 기법 면에서도 많이 부족하다. 다시 그려와.", "task": { - "hidden_keyword": "바다", - "poetic_description": "무심한 흐름이 청아한 장막을 존중하며, 세상의 마지막 이야기를 속삭이는 곳, 이를테면 그곳은 용기와 두려움이 공존하는 곳. 언젠가 잃어버린 모든 순간들이 수면 아래에서 춤추는 곳...", + "keyword": "바다", + "situation": "무심한 흐름이 청아한 장막을 존중하며, 세상의 마지막 이야기를 속삭이는 곳, 이를테면 그곳은 용기와 두려움이 공존하는 곳. 언젠가 잃어버린 모든 순간들이 수면 아래에서 춤추는 곳...", "game_id": "5055" }, "game_id": "5055" @@ -86,7 +63,7 @@ class EvaluationReq(BaseModel): } } ) -def evaluate_task(game_id: str, req: EvaluationReq = Body()): - evaluation = lulu.evaluate_drawing(game_id, req.description) +async def evaluate_task(game_id: str, req: EvaluationReq = Body()): + evaluation = await lulu.evaluate_drawing(game_id, req.description) lulu.flush_game_data(game_id) - return evaluation + return evaluation \ No newline at end of file diff --git a/src/api/masking.py b/src/api/masking.py new file mode 100644 index 0000000..be78248 --- /dev/null +++ b/src/api/masking.py @@ -0,0 +1,44 @@ +from io import BytesIO + +from fastapi import APIRouter, UploadFile, File, HTTPException +from src.core.mask import mask_text, upload_to_s3, S3_BUCKET_NAME +from PIL import Image +import uuid +router = APIRouter( + tags=["Image Masking"] +) + +@router.post( + "/upload", + summary="이미지 텍스트 마스킹 및 S3 업로드 API", + description="업로드된 이미지 파일에서 텍스트를 마스킹하고, 마스킹된 이미지를 S3에 업로드한 후 해당 이미지의 URL을 반환합니다.", + responses={ + 200: {"message": "업로드된 이미지의 S3 URL"}, + + } +) +async def mask_image(file: UploadFile = File(...)): + if not S3_BUCKET_NAME: + raise HTTPException(status_code=500, detail="S3_BUCKET_NAME 환경 변수가 설정되지 않았습니다.") + + try: + contents = await file.read() + img = Image.open(BytesIO(contents)).convert("RGB") + except Exception as e: + raise HTTPException(status_code=400, detail=f"이미지 파일을 읽는 도중 오류가 발생했습니다: {e}") + + masked_img = mask_text(img) + + masked_img_buffer = BytesIO() + masked_img.save(masked_img_buffer, format="PNG") + masked_img_buffer.seek(0) + + file_extension = file.filename.split(".")[-1] if "." in file.filename else "png" + s3_filename = f"masked_images/{uuid.uuid4()}.{file_extension}" + + try: + s3_url = upload_to_s3(masked_img_buffer, s3_filename) + except Exception as e: + raise HTTPException(status_code=500, detail=f"S3 이미지 업로드 중에 오류가 발생했습니다 : {e}") + + return {"message" : s3_url} diff --git a/src/api/myomyo.py b/src/api/myomyo.py new file mode 100644 index 0000000..bc7d0c1 --- /dev/null +++ b/src/api/myomyo.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, HTTPException, Body +from pydantic import BaseModel, Field +from typing import List +from src.core.myomyo import MyoMyoAI +import os + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +myomyo = MyoMyoAI(api_key=OPENAI_API_KEY) + +router = APIRouter( + prefix="/myomyo", + tags=["MyoMyo AI"] +) + + +class GPTResponse(BaseModel): + message: str = Field(..., description="AI가 생성한 메시지") + +# START_GAME +class GameStartReq(BaseModel): + players: List[str] = Field(..., description="게임에 참여할 플레이어 이름 List") + +@router.post("/{game_id}/start", summary="게임 시작 메시지 API") +async def start_game(game_id: str, request: GameStartReq = Body(...)): + message = await myomyo.game_start_message(game_id=game_id, players=request.players) + return GPTResponse(message=message) + +class RoundStartReq(BaseModel): + round_num: int = Field(..., description="현재 라운드 번호") + total_rounds: int = Field(..., description="전체 라운드 수") + +@router.post("/{game_id}/round/start", summary="라운드 시작 메시지 API") +async def start_round(game_id: str, request: RoundStartReq = Body(...)): + message = await myomyo.round_start_message( + game_id=game_id, + round_num=request.round_num, + total_rounds=request.total_rounds + ) + return GPTResponse(message=message) + + +class GuessStartReq(BaseModel): + round_num: int = Field(..., description="현재 라운드 번호") + total_rounds: int = Field(..., description="전체 라운드 수") + drawer: str = Field(..., description="그림을 그린 플레이어 이름") + guesser: str = Field(..., description="그림을 맞출 플레이어 이름") + +@router.post('/{game_id}/guess/start/', summary="추측 시작 시 묘묘의 도발 메시지") +async def start_guess(game_id: str, request: GuessStartReq = Body(...)): + message = await myomyo.guess_start_message(game_id=game_id, round_num=request.round_num, total_rounds=request.total_rounds, drawer=request.drawer, guesser=request.guesser) + return GPTResponse(message=message) + +class MakeGuessReq(BaseModel): + image_description: str = Field(..., description="그림에 대한 설명") + +@router.post("/{game_id}/guess", summary="AI 정답 추론 API") +async def make_guess(game_id: str, request: MakeGuessReq = Body(...)): + message = await myomyo.guess_message( + game_id=game_id, + image_description=request.image_description + ) + return GPTResponse(message=message) + +class GuessReactReq(BaseModel): + is_correct: bool = Field(..., description="추측의 정답 여부") + answer: str = Field(..., description="실제 정답") + guesser: str = Field(default=None, description="추측한 플레이어") + +@router.post("/{game_id}/guess/react", summary="예측 결과 반응 메시지 API") +async def react_to_guess(game_id: str, request: GuessReactReq = Body(...)): + message = await myomyo.react_to_guess_message( + game_id=game_id, + is_correct=request.is_correct, + guesser=request.guesser, + answer=request.answer + ) + return GPTResponse(message=message) + +class GameEndReq(BaseModel): + winner: str = Field(..., description="묘묘의 승리 여부") + +@router.post("/{game_id}/end", summary="게임 종료 메시지 API") +async def end_game(game_id: str, request: GameEndReq = 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 GPTResponse(message=message) \ No newline at end of file diff --git a/src/api/myomyo_routes.py b/src/api/myomyo_routes.py deleted file mode 100644 index dcaccad..0000000 --- a/src/api/myomyo_routes.py +++ /dev/null @@ -1,222 +0,0 @@ -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 - return "서버 점검중이다묘!" - -# 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 - return "서버 점검 중이다묘!" - - -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 - return "서버 점검 중이다묘!" - - - - - -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 - return "서버 점검중이다묘!" -# 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 - return "서버 점검중이다묘!" - -# 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 - return "서버 점검줌이다묘!" - - -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 - return "서버 점검중이다묘!" \ No newline at end of file diff --git a/src/chat/lulu.py b/src/chat/lulu.py deleted file mode 100644 index 0628113..0000000 --- a/src/chat/lulu.py +++ /dev/null @@ -1,243 +0,0 @@ -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") - task = { - "keyword": "달", - "situation": "밤이 깊어질 때, 하늘의 은밀한 친구가 창문 너머로 속삭이고 있어. 그 둥근 미소가 어둠 속에서 혼자 빛나고 있는데, 왜인지 모르게 마음이 차분해져. 그 장면, 나한테 다시 보여줄 수 있을까?", - "game_id": game_id - } - self.active_games[game_id] = task - - return task - - - # 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] - - evaluation = { - "score": 35, - "feedback": "평가 시스템에 오류가 생겻다루!", - "task": current_task, - "game_id": game_id - } - - return evaluation - # # 가장 최근 과제 가져오기 - # 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/config.py b/src/config.py new file mode 100644 index 0000000..61ec45e --- /dev/null +++ b/src/config.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel +from typing import List +import os + +class Settings(BaseModel): + CAPTIONING_MODEL: str = "Salesforce/blip-image-captioning-base" # CAPTIONING MODEL : BLIP + CLASSIFYING_MODEL_PATH: str = "models/classifying_model.pth" # CLASSIFYING MODEL : EFFICIENTNET_B0, fine-tuned with quick-draw dataset + AWS_ACCESS_KEY_ID: str = os.getenv("AWS_S3_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY: str = os.getenv("AWS_S3_SECRET_ACCESS_KEY") + AWS_REGION: str = os.getenv("AWS_S3_REGION") + S3_BUCKET_NAME: str = os.getenv("AWS_S3_BUCKET_NAME") + NUM_CLASSES: int = 100 # Number of categories in the quick-draw dataset + + ENG_CATEGORIES: List[str] = [ + "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: List[str] = [ + "항공모함", "비행기", "알람시계", "앰뷸런스", "천사", "사과", "팔", "도끼", "백팩", "바나나", + "붕대", "야구공", "농구공", "야구배트", "욕조", "침대", "꿀벌", "자전거", "새", "생일케이크", + "책", "나비넥타이", "빵", "빗자루", "양동이", "버스", "수풀", "나비", "케이크", "달력", + "카메라", "모닥불", "양초", "차", "당근", "고양이", "핸드폰", "의자", "교회", "동그라미", + "구름", "컴파스", "컴퓨터", "쿠키", "소파", "소", "게", "악어", "왕관", "컵", + "개", "돌고래", "도넛", "문", "오리", "귀", "코끼리", "편지봉투", "눈", "안경", + "얼굴", "선풍기", "소화기", "물고기", "꽃", "포크", "개구리", "프라이팬", "정원", "기린", + "포도", "기타", "망치", "모자", "헬리콥터", "육각형", "하키 채", "말", "아이스크림", "재킷", + "캥거루", "키보드", "칼", "사다리", "노트북", "나뭇잎", "다리", "등대", "번개", "사자", + "가재", "막대사탕", "우체통", "지도", "보드마카", "확성기", "달", "오토바이", "산", "머그컵" + ] + + TEXT_THRESHOLD: float = 0.5 + +settings = Settings() diff --git a/src/chat/__init__.py b/src/core/__init__.py similarity index 100% rename from src/chat/__init__.py rename to src/core/__init__.py diff --git a/src/core/caption.py b/src/core/caption.py new file mode 100644 index 0000000..5eb2b72 --- /dev/null +++ b/src/core/caption.py @@ -0,0 +1,33 @@ +from transformers import BlipProcessor, BlipForConditionalGeneration +from PIL import Image +from io import BytesIO +import torch +from src.config import settings + +print('BLIP 모델 로딩중....') +processor = BlipProcessor.from_pretrained(settings.CAPTIONING_MODEL) +model = BlipForConditionalGeneration.from_pretrained(settings.CAPTIONING_MODEL) +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +model.to(device) +print('BLIP 모델 로딩완료!') + + + +def generate_caption(image: bytes) -> str: + """ + 이미지 캡션 생성 함수 + + Args: + image (bytes): 이미지 바이트 데이터 + + Returns: + str: 생성된 캡션 + """ + image = Image.open(BytesIO(image)).convert("RGB") + inputs = processor(image, return_tensors="pt").to(device) + + with torch.no_grad(): + out = model.generate(**inputs) + + caption = processor.decode(out[0], skip_special_tokens=True) + return caption diff --git a/src/core/classify.py b/src/core/classify.py new file mode 100644 index 0000000..b016efe --- /dev/null +++ b/src/core/classify.py @@ -0,0 +1,60 @@ +from PIL import Image +from io import BytesIO +import torch +import torch.nn as nn +from torchvision import transforms as T +from torchvision.models import efficientnet_b0 +from typing import List +from src.config import settings + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +def load_model(model_path: str, num_classes: int) -> nn.Module: + model = efficientnet_b0(weights=None) + num_ftrs = model.classifier[1].in_features + model.classifier[1] = nn.Linear(num_ftrs, num_classes) + checkpoint = torch.load(model_path, map_location=device) + if 'model_state_dict' in checkpoint: + model.load_state_dict(checkpoint['model_state_dict']) + else: + model.load_state_dict(checkpoint) + model.to(device) + model.eval() + return model + + +classifier = load_model(settings.CLASSIFYING_MODEL_PATH, settings.NUM_CLASSES) + +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]) + ]) + + +def classify(image_bytes: bytes) -> List[dict]: + """ + 이미지 분류 함수 + 1. 이미지 전처리 + 2. 모델 추론 + 3. 상위 3개 카테고리 및 신뢰도 반환 + 4. 한글 카테고리로 매핑하여 반환 + 5. 반환 형식: List[{"predicted": 카테고리, "confidence": 신뢰도}] + 6. 신뢰도는 퍼센트(%)로 반환 + """ + image = Image.open(BytesIO(image_bytes)).convert("RGB") + img_tensor = encode_image(image).unsqueeze(0).to(device) + with torch.no_grad(): + outputs = classifier(img_tensor) + probabilities = torch.nn.functional.softmax(outputs[0], dim=0) + top3_prob, top3_catid = torch.topk(probabilities, 3) + results = [] + for i in range(top3_prob.size(0)): + results.append({ + "predicted": settings.KOR_CATEGORIES[top3_catid[i]], # 한글 카테고리로 변경 + "confidence": top3_prob[i].item() * 100 + }) + return results \ No newline at end of file diff --git a/src/core/lulu.py b/src/core/lulu.py new file mode 100644 index 0000000..a268b06 --- /dev/null +++ b/src/core/lulu.py @@ -0,0 +1,209 @@ +from openai import AsyncOpenAI +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 = AsyncOpenAI(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] + + + async 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 = await 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: # json 파싱 오류 등 + raise RuntimeError(f"Error generating drawing task: {e}") # 상위에서 처리하도록 예외 던짐, todo: 전역 핸들러 추가 + + + + async 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 = await 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: # json 파싱 오류 등 + raise RuntimeError(f"Error evaluating drawing: {e}") # 상위에서 처리하도록 예외 던짐, todo: 전역 핸들러 추가 \ No newline at end of file diff --git a/src/core/mask.py b/src/core/mask.py new file mode 100644 index 0000000..162a410 --- /dev/null +++ b/src/core/mask.py @@ -0,0 +1,53 @@ +from PIL import Image, ImageDraw +import easyocr +import numpy as np +import boto3 +from src.config import settings + +s3_client = boto3.client( + 's3', + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.AWS_REGION +) +S3_BUCKET_NAME = settings.S3_BUCKET_NAME + +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 >= settings.TEXT_THRESHOLD] + + # for i, box in enumerate(filtered_boxes): + # print(f"[{i + 1}] 박스 좌표 (신뢰도 ≥ {settings.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 + +def upload_to_s3(buffer, filename): + s3_client.upload_fileobj(buffer, S3_BUCKET_NAME, filename) + return f"https://{S3_BUCKET_NAME}.s3.amazonaws.com/{filename}" + + + + diff --git a/src/chat/myomyo.py b/src/core/myomyo.py similarity index 98% rename from src/chat/myomyo.py rename to src/core/myomyo.py index 4c31f5c..621a4e5 100644 --- a/src/chat/myomyo.py +++ b/src/core/myomyo.py @@ -1,6 +1,6 @@ from typing import Dict, List from threading import Lock -from openai import OpenAI +from openai import AsyncOpenAI class MyoMyoAI: """ @@ -28,7 +28,7 @@ def __init__(self, api_key: str, model: str = "gpt-3.5-turbo"): with self._lock: if self._initialized: return - self.client = OpenAI(api_key=api_key) + self.client = AsyncOpenAI(api_key=api_key) self.model = model self._initialized = True self.game_histories = {} # game_id로 구분됨 @@ -109,7 +109,7 @@ async def generate_response(self, game_id: str, prompt: str, role: str = "system }) try: - responses = self.client.chat.completions.create( + responses = await self.client.chat.completions.create( model = self.model, messages = messages, temperature = 0.8, # 모델 출력의 무작위성 제어 @@ -198,7 +198,7 @@ async def react_to_guess_message(self, game_id: str, is_correct: bool, answer: s 묘묘의 반응 """ - if guesser == '묘묘' or guesser is None: + if guesser == 'AI' or guesser is None: # 묘묘의 추측 prompt = f"""너(묘묘)가 방금 추측을 했어. {f"정답은 '{answer}'야" if is_correct else ""}. 너의 추측은 {'맞았어' if is_correct else '틀렸어'}. 이 결과에 대한 너의 반응을 짧고 도발적으로 말해줘.""" diff --git a/src/image/__init__.py b/src/image/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/image/classifier.py b/src/image/classifier.py deleted file mode 100644 index 3575698..0000000 --- a/src/image/classifier.py +++ /dev/null @@ -1,116 +0,0 @@ -# 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 deleted file mode 100644 index b9a8776..0000000 --- a/src/image/img_caption.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index f8dbec7..0000000 --- a/src/image/model.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index f8a6ea4..0000000 --- a/src/image/preprocessor.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index c32b123..0000000 --- a/src/image/text_masking.py +++ /dev/null @@ -1,42 +0,0 @@ -# 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/main.py b/src/main.py index 2b096e0..bd35e06 100644 --- a/src/main.py +++ b/src/main.py @@ -1,19 +1,31 @@ -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 +from src.api.captioning import router as caption_router +from src.api.myomyo import router as myomyo_router +from src.api.lulu import router as lulu_router +from src.api.classifying import router as classification_router +from src.api.masking import router as masking_router +import httpx + +async def lifespan(app): + app.state.http = httpx.AsyncClient( + timeout=httpx.Timeout(10.0), + limits=httpx.Limits(max_keepalive_connections=100, max_connections=200), + ) + yield + await app.state.http.aclose() + + app = FastAPI( title="Gotcha! AI Server", description="AI Server", docs_url="/docs", openapi_url="/openapi.json", - redoc_url="/redoc" + redoc_url="/redoc", + lifespan=lifespan, ) -app.include_router(image_router, prefix='/api/v1') - -app.include_router(chat_router, prefix='/api/v1') - - +app.include_router(caption_router, prefix='/api/v1') +app.include_router(classification_router, prefix='/api/v1') +app.include_router(masking_router, prefix='/api/v1') +app.include_router(myomyo_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 deleted file mode 100644 index 3199b7c..0000000 Binary files a/train/img.png and /dev/null differ diff --git a/train/readme.md b/train/readme.md deleted file mode 100644 index 6ea509a..0000000 --- a/train/readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# 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 deleted file mode 100644 index 8f50f97..0000000 --- a/train/train_efficientnet_b0.ipynb +++ /dev/null @@ -1,1458 +0,0 @@ -{ - "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 -}