ccfdac1286
2. fastAPI로 프로젝트 구조 실습 1. 랭체인 이미지 인식 후 처리 마무리 2. fastAPI로 프로젝트 구조 실습
835 lines
32 KiB
Plaintext
835 lines
32 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"### Vision 모델 내부 동작 원리\n",
|
|
"- 이미지 인코딩 : 이미지 임베딩 벡터\n",
|
|
"- 프로젝션 : 이미지 벡터를 LLM 토큰 공간으로 변환\n",
|
|
"- 토큰 결합 : 시각 토큰 + 텍스트 토큰을 순서대로 배열\n",
|
|
"- LLM 처리\n",
|
|
"- 텍스트 생성"
|
|
],
|
|
"id": "3ed294a9c2fd2cf5"
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"id": "initial_id",
|
|
"metadata": {
|
|
"collapsed": true,
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:07.002963942Z",
|
|
"start_time": "2026-06-15T03:06:05.360626592Z"
|
|
}
|
|
},
|
|
"source": [
|
|
"import base64\n",
|
|
"\n",
|
|
"from jedi.inference import analysis\n",
|
|
"from langchain_core import documents\n",
|
|
"from langchain_ibm import WatsonxEmbeddings\n",
|
|
"from langchain_ibm import ChatWatsonx\n",
|
|
"\n",
|
|
"from dotenv import load_dotenv\n",
|
|
"from langchain_core.prompts import ChatPromptTemplate\n",
|
|
"from langchain_core.output_parsers import StrOutputParser\n",
|
|
"\n",
|
|
"from langchain_chroma import Chroma\n",
|
|
"import os\n",
|
|
"\n",
|
|
"from langchain_ollama import ChatOllama\n",
|
|
"from pathlib import Path\n",
|
|
"\n",
|
|
"from langchain_core.documents import Document\n",
|
|
"from langchain_core.messages import HumanMessage"
|
|
],
|
|
"outputs": [],
|
|
"execution_count": 3
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:07.012203786Z",
|
|
"start_time": "2026-06-15T03:06:07.003754555Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# .env 내용 가져오기\n",
|
|
"load_dotenv()\n",
|
|
"\n",
|
|
"apikey = os.getenv(\"WATSONX_API_KEY\")\n",
|
|
"project_id = os.getenv(\"WATSONX_PROJECT_ID\")\n",
|
|
"watsonx_ai_url = os.getenv(\"WATSONX_URL\")"
|
|
],
|
|
"id": "27bd39a0da002140",
|
|
"outputs": [],
|
|
"execution_count": 4
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:09.243124960Z",
|
|
"start_time": "2026-06-15T03:06:07.013041549Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"watson_llm = ChatWatsonx(\n",
|
|
" model_id=\"ibm/granite-4-h-small\",\n",
|
|
" url=f\"{watsonx_ai_url}\",\n",
|
|
" api_key=f\"{apikey}\",\n",
|
|
" project_id=f\"{project_id}\",\n",
|
|
" max_tokens=2000,\n",
|
|
" params={\n",
|
|
" \"temperature\": 0\n",
|
|
" }\n",
|
|
")\n",
|
|
"\n",
|
|
"watson_embedding = WatsonxEmbeddings(\n",
|
|
" model_id=\"ibm/granite-embedding-278m-multilingual\",\n",
|
|
" url=f\"{watsonx_ai_url}\",\n",
|
|
" api_key=f\"{apikey}\",\n",
|
|
" project_id=f\"{project_id}\"\n",
|
|
")\n",
|
|
"\n",
|
|
"# 로컬 LLM\n",
|
|
"qwen_llm = ChatOllama(model=\"qwen3.5:4b\", temperature=0)\n",
|
|
"exaone_llm = ChatOllama(model=\"exaone3.5:2.4b\", temperature=0)\n",
|
|
"gemma_llm = ChatOllama(model=\"gemma4:e2b\")"
|
|
],
|
|
"id": "1b23141c1b60154a",
|
|
"outputs": [],
|
|
"execution_count": 5
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": "#### 1. 로컬 이미지",
|
|
"id": "ef95d69bf02f830b"
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:09.262869259Z",
|
|
"start_time": "2026-06-15T03:06:09.252393224Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# images를 base64 인코딩\n",
|
|
"\n",
|
|
"def encode_image(image_path:str)->str:\n",
|
|
" with open(image_path, \"rb\") as f:\n",
|
|
" return base64.b64encode(f.read()).decode(\"utf-8\")"
|
|
],
|
|
"id": "637e09cdf9c78eee",
|
|
"outputs": [],
|
|
"execution_count": 6
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:09.329919223Z",
|
|
"start_time": "2026-06-15T03:06:09.264035014Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# vision 모델\n",
|
|
"vision_llm = ChatOllama(model=\"minimax-m3:cloud\", temperature=0)\n",
|
|
"\n",
|
|
"parser = StrOutputParser()\n",
|
|
"\n",
|
|
"# 이미지 + 텍스트 : 이미지 기반으로 질문\n",
|
|
"def ask_about_image(image_path, question):\n",
|
|
" image_b64 = encode_image(image_path)\n",
|
|
" prompt = ChatPromptTemplate.from_messages(\n",
|
|
" [\n",
|
|
" (\"human\",\n",
|
|
" [\n",
|
|
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{image_b64}'}},\n",
|
|
" {\"type\":\"text\", \"text\":question}\n",
|
|
" ]\n",
|
|
" )\n",
|
|
" ]\n",
|
|
" )\n",
|
|
"\n",
|
|
" chain = prompt | vision_llm\n",
|
|
" response = chain.invoke({\"image_b64\":image_b64, \"question\":question})\n",
|
|
" return response"
|
|
],
|
|
"id": "a1114bd96f354025",
|
|
"outputs": [],
|
|
"execution_count": 7
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:21.590918755Z",
|
|
"start_time": "2026-06-15T03:06:09.330837660Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": "print(ask_about_image(\"./image/animals1.jpg\", \"이 이미지에서 무엇이 보이나요?\"))",
|
|
"id": "25f2f6918598e8e0",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"content='이 이미지에서는 카메라를 올려다보는 고양이가 보입니다. 주요 특징은 다음과 같습니다:\\n\\n- **품종**: 얼룩무늬 고양이(태비)로, 회색, 검은색, 갈색이 섞인 줄무늬 패턴을 가지고 있습니다.\\n- **눈**: 크고 둥근 밝은 청록색/연한 녹색 눈동자로, 위쪽에서 비스듬히 내려다보는 각도로 촬영되었습니다.\\n- **표정**: 입이 살짝 벌어져 있어 마치 울거나 야옹하는 것처럼 보이며, 작은 이빨이 살짝 보입니다.\\n- **수염**: 길고 곧은 흰색 수염이 양쪽으로 뻗어 있습니다.\\n- **배경**: 따뜻한 톤의 나무 바닥이나 가구 위에 있는 것으로 보입니다.\\n\\n전체적으로 아기자기하면서도 약간 놀라 보이거나 무언가를 요구하는 듯한 표정의 고양이 클로즈업 사진입니다.' additional_kwargs={} response_metadata={'model': 'minimax-m3', 'created_at': '2026-06-15T03:06:21.490583355Z', 'done': True, 'done_reason': 'stop', 'total_duration': 11626185191, 'load_duration': None, 'prompt_eval_count': 511, 'prompt_eval_duration': None, 'eval_count': 231, 'eval_duration': None, 'logprobs': None, 'model_name': 'minimax-m3', 'model_provider': 'ollama'} id='lc_run--019ec93e-624f-7b11-89b0-cd5aabe517b3-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 511, 'output_tokens': 231, 'total_tokens': 742}\n"
|
|
]
|
|
}
|
|
],
|
|
"execution_count": 8
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:21.612768204Z",
|
|
"start_time": "2026-06-15T03:06:21.600081143Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": "#### 2. URL 이미지",
|
|
"id": "79877a4f0166e70d",
|
|
"outputs": [],
|
|
"execution_count": 9
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:57.034661130Z",
|
|
"start_time": "2026-06-15T03:06:21.613674680Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"import requests\n",
|
|
"from io import BytesIO\n",
|
|
"from PIL import Image\n",
|
|
"\n",
|
|
"# 이미지 다운로드\n",
|
|
"url ='https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQK4_aoipvzj-4LsUx-FAtPmRnkgtOOMbRAQOfBceT5UQ&s=10'\n",
|
|
"img = requests.get(url).content\n",
|
|
"# 인코딩\n",
|
|
"img_b64 = base64.b64encode(img).decode(\"utf-8\")\n",
|
|
"\n",
|
|
"# 모델\n",
|
|
"message = HumanMessage(\n",
|
|
" content = [\n",
|
|
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
|
" {\"type\":\"text\", \"text\": \"이 동물의 종류와 특징을 설명해주세요.\"}\n",
|
|
" ]\n",
|
|
" )\n",
|
|
"\n",
|
|
"vision_llm.invoke([message]).content"
|
|
],
|
|
"id": "bcc767a8ef31098b",
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"'# 🐱 고양이 품종 분석\\n\\n이미지에 보이는 고양이는 **장모종(Long-haired breed)** 의 새끼 고양이로 보입니다.\\n\\n## 추정 품종\\n\\n### 1. 시베리안 고양이 (Siberian Cat)\\n가장 가능성이 높은 품종입니다.\\n- **러시아 원산**의 자연산 출생 품종\\n- 풍성한 이중 모피(Double coat)\\n- 추운 기후에 적응한 풍성한 꼬리(브러시 테일)\\n\\n### 2. 노르웨이 숲고양이 (Norwegian Forest Cat)\\n가능성이 있는 대체 품종\\n- **북유럽(노르웨이) 원산**\\n- 비슷한 외형의 대형 장모종\\n\\n### 3. 메인쿤 (Maine Coon)\\n가능성 있음\\n- 미국 원산의 대형 품종\\n- 귀 끝에 술(lynx tips)이 있는 것이 특징\\n\\n## 외형적 특징\\n\\n| 특징 | 설명 |\\n|------|------|\\n| **털 색깔** | 그레이 & 화이트 이색(Bicolor) |\\n| **모피** | 길고 부드러운 장모, 이중 코트 |\\n| **귀** | 크고 뾰족하며 끝에 작은 술이 있음 |\\n| **눈** | 크고 둥글며 밝은 색(황록색) |\\n| **체형** | 통통하고 근육질, 강건한 골격 |\\n| **꼬리** | 매우 풍성한 브러시 형태 |\\n\\n## 성격적 특징 (장모종 공통)\\n\\n- 🌟 **온화하고 친근한 성격**\\n- 🧠 **높은 지능과 호기심**\\n- 💧 **물을 좋아하는 편** (대부분의 고양이와 다름)\\n- 👨\\u200d👩\\u200d👧 **가족에게 애착이 강함**\\n- 🐾 **활동적이고 장난기 많음**\\n\\n## 관리 포인트\\n\\n- **정기적인 빗질**: 매일 1회, 털 엉킴 방지\\n- **철저한 위생 관리**: 장모는 배변 후 엉덩이 털 관리가 필요\\n- **운동 공간 확보**: 대형 품종으로 충분히 뛰어놀 수 있는 공간 필요\\n- **식단 관리**: 풍성한 모피 유지를 위한 고단백질 사료\\n\\n이처럼 사랑스럽고 아름다운 새끼 고양이는 성격이 매우 좋고, 가족과 함께하면 훌륭한 반려동물이 될 것입니다! 💕'"
|
|
]
|
|
},
|
|
"execution_count": 10,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"execution_count": 10
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:57.059273077Z",
|
|
"start_time": "2026-06-15T03:06:57.045190160Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": "#### 3. OCR",
|
|
"id": "9549854d2a4d9c89",
|
|
"outputs": [],
|
|
"execution_count": 11
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:06:57.080602805Z",
|
|
"start_time": "2026-06-15T03:06:57.060282470Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# 모든 이미지를 jpeg 저장 / 인코딩\n",
|
|
"\n",
|
|
"def pil_to_base64(img:Image.Image,format=\"JPEG\")->str:\n",
|
|
" buffer = BytesIO()\n",
|
|
" img.save(buffer, format=format)\n",
|
|
"\n",
|
|
" return base64.b64encode(buffer.getvalue()).decode(\"utf-8\")"
|
|
],
|
|
"id": "32f3f6d75760fcec",
|
|
"outputs": [],
|
|
"execution_count": 12
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:07:15.435586589Z",
|
|
"start_time": "2026-06-15T03:06:57.083111604Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"img = Image.open(\"./image/system.png\")\n",
|
|
"\n",
|
|
"# 리사이즈\n",
|
|
"img_resized = img.resize((800,600))\n",
|
|
"\n",
|
|
"# Gray\n",
|
|
"img_gray = img_resized.convert(\"L\")\n",
|
|
"img_b64 = pil_to_base64(img_gray)\n",
|
|
"\n",
|
|
"message = HumanMessage(\n",
|
|
" content = [\n",
|
|
" {\"type\":\"text\", \"text\":\"이 문서의 이미지에서 텍스트를 추출해주세요\"},\n",
|
|
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
|
"\n",
|
|
" ]\n",
|
|
" )\n",
|
|
"\n",
|
|
"result = vision_llm.invoke([message]).content\n",
|
|
"print(result)"
|
|
],
|
|
"id": "18e51689e189291f",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"# 문서 텍스트 추출\n",
|
|
"\n",
|
|
"## 미래로봇추진단(서울 근무)\n",
|
|
"### S/W개발 - 시스템/소프트웨어\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**포지션 소개 (Job Overview)**\n",
|
|
"휴머노이드 로봇의 실시간 제어 시스템 및 소프트웨어 플랫폼을 개발하는 직무입니다. 운영체제 환경 구성, 디바이스 드라이버, 실시간 제어 프레임워크 등 로봇 동작의 핵심 기반이 되는 소프트웨어를 설계 및 개발하며, 하드웨어 설계 조직 및 AI 연구 조직과 긴밀히 협업합니다.\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**수행업무 (Job Details)**\n",
|
|
"- 휴머노이드 로봇의 실시간 제어 프레임워크를 설계하고 개발합니다.\n",
|
|
"- 로보, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
|
|
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경에 구성하고 시스템 성능을 최적화합니다.\n",
|
|
"- 유관 부서와 협업하여 로봇 조직 등 시 기능과 제어 시스템 간 연동 미들웨어를 개발합니다.\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**자격요건 (Requirements)**\n",
|
|
"- 컴퓨터, 전기·전자, 기계, 로봇공학 등 관련 전공을 하신 분\n",
|
|
"- 운영체제 기반 개념에 대한 이해도를 보유하신 분\n",
|
|
"- 요구사항을 분석하여 소프트웨어를 구조적으로 설계 및 구현하는 역량을 보유하신 분\n",
|
|
"- 다양한 분야의 엔지니어와 적극적으로 소통하며 협업할 수 있는 역량을 보유하신 분\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**우대사항 (Preferences)**\n",
|
|
"- C/C++ 기반 시스템 프로그래밍 역량을 보유하신 분\n",
|
|
"- Linux 기반 임베디드 시스템 개발 경험을 보유하신 분 (Kernel, Device Driver, BSP 등)\n",
|
|
"- CAN, EtherCAT, UDP 등 하드웨어 통신 프로토콜 활용 경험을 보유하신 분\n",
|
|
"- ROS2 기반 로봇제어 소프트웨어 수행 경험을 보유하신 분\n",
|
|
"- Git 기반 협업 및 CI/CD 환경에서의 개발 경험을 보유하신 분\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**커리어 비전 (Career Vision)**\n",
|
|
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇틱스 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"> **\"휴머노이드의 미래를 함께 설계할 분을 찾습니다\"**\n",
|
|
"> \n",
|
|
"> 로봇이 인정적으로 동작하기 위한 소프트웨어 기반을 함께 만들어가자. 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
|
|
]
|
|
}
|
|
],
|
|
"execution_count": 13
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:07:15.454781421Z",
|
|
"start_time": "2026-06-15T03:07:15.444138572Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# 다중 이미지 비교분석\n",
|
|
"\n",
|
|
"def compare_images(image_paths, question):\n",
|
|
" content = []\n",
|
|
"\n",
|
|
" for i, path in enumerate(image_paths, 1):\n",
|
|
" img_b64 = encode_image(path)\n",
|
|
" content.append({\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}})\n",
|
|
" content.append({\"type\":\"text\", \"text\": f\"[이미지 {i}]\"})\n",
|
|
"\n",
|
|
" # 질문 추가\n",
|
|
" content.append({\"type\":\"text\", \"text\":question})\n",
|
|
" message = HumanMessage(content=content)\n",
|
|
" response = vision_llm.invoke([message])\n",
|
|
" return response.content"
|
|
],
|
|
"id": "d184430da51ccc36",
|
|
"outputs": [],
|
|
"execution_count": 14
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:07:32.850368373Z",
|
|
"start_time": "2026-06-15T03:07:15.455557487Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"result = compare_images(['./image/fridge.jpg', './image/table.jpg'], \"두 제품의 차이점을 비교 분석해 주세요.\")\n",
|
|
"print(result)"
|
|
],
|
|
"id": "baa2ba7a8d4a6b10",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"# 두 제품 비교 분석\n",
|
|
"\n",
|
|
"## 🖼️ 이미지 1: 냉장고 (4도어 프렌치 도어형)\n",
|
|
"\n",
|
|
"### 주요 특징\n",
|
|
"- **구조**: 4도어 프렌치 도어 디자인 (상단 2개 + 하단 2개)\n",
|
|
"- **색상**: 화이트 / 크림색\n",
|
|
"- **디자인**: 미니멀하고 깔끔한 라인\n",
|
|
"- **디스플레이**: 중앙에 검은색 디지털 디스플레이 패널\n",
|
|
"- **바퀴**: 하단 조절 가능한 발\n",
|
|
"\n",
|
|
"### 용도\n",
|
|
"주방용 대형 가전제품 (식품 보관)\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"## 🖼️ 이미지 2: 식탁 / 다이닝 테이블\n",
|
|
"\n",
|
|
"### 주요 특징\n",
|
|
"- **소재**: 대리석 (화이트 + 그레이 veins) 상판\n",
|
|
"- **다리**: 검은색/다크 컬러 + 골드(Gold) 팁\n",
|
|
"- **형태**: 직사각형, 라운드 코너 처리\n",
|
|
"- **구조**: 모던한 4각 다리형\n",
|
|
"- **수납**: 하단 선반 구조\n",
|
|
"\n",
|
|
"### 용도\n",
|
|
"거실/식당/다이닝 공간 가구\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"## 📊 핵심 차이점 비교\n",
|
|
"\n",
|
|
"| 구분 | 냉장고 | 식탁 |\n",
|
|
"|------|--------|------|\n",
|
|
"| **카테고리** | 가전제품 | 가구 |\n",
|
|
"| **소재** | 금속/플라스틱 | 대리석 + 금속 |\n",
|
|
"| **용도** | 식품 보관·냉각 | 식사·작업 공간 |\n",
|
|
"| **크기** | 대형 (약 180cm+) | 중형 (140~180cm) |\n",
|
|
"| **가격대** | 고가 (100만원~) | 중~고가 |\n",
|
|
"| **색감** | 모노톤 화이트 | 화이트+블랙+골드 |\n",
|
|
"| **디자인 스타일** | 미니멀 모던 | 럭셔리 모던 |\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"## 🎨 공통점\n",
|
|
"- **모던한 디자인**: 두 제품 모두 현대적인 인테리어에 어울리는 깔끔한 스타일\n",
|
|
"- **화이트 톤**: 밝고 깨끗한 인상을 주는 화이트 계열\n",
|
|
"- **고급스러움**: 프리미엄 제품군의 디자인 철학\n",
|
|
"\n",
|
|
"## 💡 인테리어 활용\n",
|
|
"두 제품 모두 **모던 미니멀** 또는 **럭셔리 모던** 스타일 주방에 잘 어울리며, 화이트 톤의 통일성으로 공간의 시너지를 극대화할 수 있습니다.\n"
|
|
]
|
|
}
|
|
],
|
|
"execution_count": 15
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:08:00.503202012Z",
|
|
"start_time": "2026-06-15T03:07:32.869952272Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"result = compare_images(['./image/chart1.png', './image/chart2.png'], \"두 제품의 차트를을 비교하여 주요 변화 추이를 설명해주세요.\")\n",
|
|
"print(result)"
|
|
],
|
|
"id": "891a618c4e36fea9",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"# 두 차트 비교 분석\n",
|
|
"\n",
|
|
"## 📊 차트 1: 최근 6개월 전체 화장품 수출 추이\n",
|
|
"\n",
|
|
"| 기간 | 수출액(백만$) | YoY 증감률 |\n",
|
|
"|------|---------------|------------|\n",
|
|
"| 25년 12월 | 883.7 | +10.2% |\n",
|
|
"| 26년 1월 | 841.6 | **+33.7%** (최고) |\n",
|
|
"| 26년 2월 | 752.1 | +1.7% (최저) |\n",
|
|
"| 26년 3월 | 960.4 | +23.0% |\n",
|
|
"| 26년 4월 | **1,096.3** (최고 수출액) | +21.7% |\n",
|
|
"| 26년 5월(1~20일) | 671.1 | **-16.0%** (전월 대비 급락) |\n",
|
|
"\n",
|
|
"## 📊 차트 2: 5월 1~20일 주요 국가별 일평균 수출액\n",
|
|
"\n",
|
|
"| 국가 | 일평균 수출액(백만$) | YoY 증감률 |\n",
|
|
"|------|---------------------|------------|\n",
|
|
"| 중화권(중국) | 13.13 | **+76.4%** (최고) |\n",
|
|
"| 미국 | 11.66 | +40.3% |\n",
|
|
"| 유럽 | 11.14 | +61.3% |\n",
|
|
"| 동남아 | 4.91 | +22.0% |\n",
|
|
"| 일본 | 3.xx | **-14.1%** (유일한 마이너스) |\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"## 🔍 주요 변화 추이 분석\n",
|
|
"\n",
|
|
"### 1️⃣ **전체 수출의 5월 급격한 반전**\n",
|
|
"- 1~4월은 두 자릿수 성장(YoY +20~33%)을 유지하며 **우상향 흐름**이었으나, 5월(1~20일) 들어 **-16% 마이너스 성장**으로 전환\n",
|
|
"- 4월 대비 약 425백만$ 감소하며 **6개월 중 최저 수출액** 기록\n",
|
|
"\n",
|
|
"### 2️⃣ **국가별 양극화 현상**\n",
|
|
"- ✅ **호조**: 중화권(76.4%), 유럽(61.3%), 미국(40.3%) 등 신흥·대형 시장은 여전히 고성장 지속\n",
|
|
"- ❌ **악화**: 일본은 유일한 마이너스(-14.1%) 국가로, **일·중 갈등 리스크**(수출 규제 등)가 직접적 영향으로 작용한 것으로 추정\n",
|
|
"\n",
|
|
"### 3️⃣ **시장 다변화 효과**\n",
|
|
"- 5월 전체 수출 급감에도 **중국·유럽·미국** 비중이 절대적(1일 평균 11백만$ 이상)이라, 일본 위축이 전체 실적을 끌어내린 구조\n",
|
|
"- 중화권 일평균 수출액(13.13M$)이 전체 5월 실적의 약 **39%**(671.1M$ ÷ 20일 = 33.6M$ 기준)를 차지\n",
|
|
"\n",
|
|
"### 4️⃣ **핵심 인사이트**\n",
|
|
"> 📌 **\"성장 동력은 견고하나, 지정학적 리스크 노출도 확대\"**\n",
|
|
"> - 美·유럽·중국의 K-뷰티 수요는 여전히 강력\n",
|
|
"> - 그러나 **일본 1개국의 감소**가 전체 수치를 마이너스로 전환시킬 정도로 **일본 시장 의존도 리스크**가 부각\n",
|
|
"> - 향후 **대일 수출 정상화 여부**가 6월 이후 실적 반등의 핵심 변수가 될 전망\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"**요약**: 두 차트를 종합하면, 2026년 상반기 화장품 수출은 **1~4월 고성장 → 5월 급락**의 V자 변동을 보였으며, 이는 **신흥 시장 확대(긍정)**와 **일본 시장 위축(부정)**의 상반된 요인이 결합된 결과로 해석됩니다.\n"
|
|
]
|
|
}
|
|
],
|
|
"execution_count": 16
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"### 멀티모달\n",
|
|
" - 이미지 기반 문서 분석 시스템"
|
|
],
|
|
"id": "9687a7578057efa4"
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:32.239301275Z",
|
|
"start_time": "2026-06-15T03:32:32.212206906Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# 이미지 => 설명문 생성\n",
|
|
"\n",
|
|
"def image_to_caption(image_path):\n",
|
|
" img_b64 = encode_image(image_path)\n",
|
|
" message = HumanMessage(\n",
|
|
" content = [\n",
|
|
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
|
" {\"type\":\"text\", \"text\": \"\"\"\n",
|
|
" 이 이미지를 검색용 설명문으로 요약하세요.\n",
|
|
" 200자 이내로 작성하세요.\n",
|
|
" 핵심 객체와 텍스트만 포함하세요.\n",
|
|
"\"\"\"}\n",
|
|
" ]\n",
|
|
" )\n",
|
|
" return vision_llm.invoke([message]).content"
|
|
],
|
|
"id": "972db1f4ffac7c6f",
|
|
"outputs": [],
|
|
"execution_count": 32
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:43.666106534Z",
|
|
"start_time": "2026-06-15T03:32:32.758731810Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"caption = image_to_caption('./image/chart1.png')\n",
|
|
"caption"
|
|
],
|
|
"id": "5b60e8a76d1a4d47",
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"'최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.'"
|
|
]
|
|
},
|
|
"execution_count": 33,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"execution_count": 33
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:43.681737845Z",
|
|
"start_time": "2026-06-15T03:32:43.668688923Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# Document\n",
|
|
"\n",
|
|
"def build_multimodal_index(image_dir:str, text_docs:list[Document]):\n",
|
|
" all_docs = list(text_docs)\n",
|
|
"\n",
|
|
" # image_dir 안 파일 가져오기\n",
|
|
" image_files = list(Path(image_dir).glob(\"*.{jpg, jpeg, png}\"))\n",
|
|
"\n",
|
|
" # 캡션 생성 => Document => 임베딩\n",
|
|
" for img_path in image_files:\n",
|
|
" caption = image_to_caption(image_path = img_path)\n",
|
|
"\n",
|
|
" doc = Document(page_content=caption, metadata={\n",
|
|
" \"source\" : str(img_path),\n",
|
|
" \"type\" : \"image\",\n",
|
|
" \"image_path\" : str(img_path)\n",
|
|
" })\n",
|
|
" all_docs.append(doc)\n",
|
|
"\n",
|
|
" Chroma.from_documents(all_docs, watson_embedding, persist_directory=\"./db/multimodal_db\")"
|
|
],
|
|
"id": "96b45e3ca155e9ab",
|
|
"outputs": [],
|
|
"execution_count": 34
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:43.691778483Z",
|
|
"start_time": "2026-06-15T03:32:43.682586147Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"# 검색결과에 이미지 포함 여부 확인\n",
|
|
"\n",
|
|
"def search_with_images(vectorstore, query):\n",
|
|
" results = vectorstore.similarity_search_with_relevance_scores(query, k=5)\n",
|
|
" text_results = [doc for doc, score in results if doc.metadata.get(\"type\") != 'image']\n",
|
|
" image_results = [doc for doc, score in results if doc.metadata.get(\"type\") == 'image']\n",
|
|
" print(f\"텍스트 결과 : {len(text_results)}, 이미지 결과 {len(image_results)}\")\n",
|
|
" return text_results, image_results"
|
|
],
|
|
"id": "8d26ef630c80b820",
|
|
"outputs": [],
|
|
"execution_count": 35
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:44.606069502Z",
|
|
"start_time": "2026-06-15T03:32:43.694865760Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"image_doc = Document(\n",
|
|
" page_content=caption,\n",
|
|
" metadata = {\"type\" : \"image\", \"image_path\":\"./image/chart1.png\"}\n",
|
|
")\n",
|
|
"\n",
|
|
"docs = [\n",
|
|
" Document(\n",
|
|
" page_content=\"2025년 매출은 증가했다.\"\n",
|
|
" ),\n",
|
|
" image_doc\n",
|
|
"]\n",
|
|
"\n",
|
|
"build_multimodal_index(\"./image\", docs)"
|
|
],
|
|
"id": "a8beb26afaf51f76",
|
|
"outputs": [],
|
|
"execution_count": 36
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:46.252600621Z",
|
|
"start_time": "2026-06-15T03:32:44.616669574Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"vectorstore = Chroma(embedding_function=watson_embedding, persist_directory=\"./db/multimodal_db\")\n",
|
|
"\n",
|
|
"results = vectorstore.similarity_search(\"매출 추세\", k=3)\n",
|
|
"\n",
|
|
"for r in results:\n",
|
|
" print(r.page_content)"
|
|
],
|
|
"id": "16976c61e211677c",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"2025년 매출은 증가했다.\n",
|
|
"2024년 분기별 매출 현황 막대그래프. Q1 120백만원(파랑), Q2 185백만원(초록), Q3 160백만원(주황), Q4 230백만원(빨강)으로 Q4가 최고치를 기록. 상단 좌측에 \"Q4 매출액 최고치\" 데이터 분석 메모 포함. 단위는 백만원.\n",
|
|
"최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.\n"
|
|
]
|
|
}
|
|
],
|
|
"execution_count": 37
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:32:46.278685884Z",
|
|
"start_time": "2026-06-15T03:32:46.264136623Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": [
|
|
"def multimodal_answer(vectorstore, question):\n",
|
|
" text_results, image_results = search_with_images(vectorstore = vectorstore, query = question)\n",
|
|
"\n",
|
|
" # 텍스트 결과 하나의 컨텍스트로 생성\n",
|
|
" text_context = \"\\n\\n\".join([r.page_content for r in text_results])\n",
|
|
"\n",
|
|
" # 이미지로 검색된 경우\n",
|
|
" image_context = \"\"\n",
|
|
" referenced_images = []\n",
|
|
" for img_doc in image_results[:3]:\n",
|
|
" img_path = img_doc.metadata.get(\"image_path\")\n",
|
|
"\n",
|
|
" img_b64 = encode_image(img_path)\n",
|
|
" analysis = vision_llm.invoke([\n",
|
|
" HumanMessage(\n",
|
|
" content=[\n",
|
|
" {\"type\": \"image_url\", \"image_url\": {'url': f'data:image/jpeg;base64,{img_b64}'}},\n",
|
|
" {\"type\": \"text\", \"text\": f\"이 이미지에서 다음 질문과 관련된 내용을 설명하세요: {question}\"}\n",
|
|
" ]\n",
|
|
" )\n",
|
|
" ]).content\n",
|
|
"\n",
|
|
" image_context += f\"[이미지 분석: {img_path}]\\n{analysis}\\n\\n\"\n",
|
|
" referenced_images.append(img_path)\n",
|
|
"\n",
|
|
" # 최종 답변\n",
|
|
" combined_context = text_context + \"\\n\\n\" + image_context\n",
|
|
"\n",
|
|
" final_prompt = ChatPromptTemplate.from_messages([\n",
|
|
" (\"system\", \"다음은 텍스트와 이미지 분석 결과를 참고하여 질문에 대한 답변입니다.\\n\\n{context}\"),\n",
|
|
" (\"human\", \"{question}\")\n",
|
|
" ])\n",
|
|
"\n",
|
|
" parser = StrOutputParser()\n",
|
|
" chain = final_prompt | watson_llm | parser\n",
|
|
"\n",
|
|
" answer = chain.invoke({\n",
|
|
" \"context\": combined_context,\n",
|
|
" \"question\": question\n",
|
|
" })\n",
|
|
"\n",
|
|
" return {\"answer\" : answer, \"images\": referenced_images}"
|
|
],
|
|
"id": "4a82feae327cb3a1",
|
|
"outputs": [],
|
|
"execution_count": 38
|
|
},
|
|
{
|
|
"metadata": {
|
|
"ExecuteTime": {
|
|
"end_time": "2026-06-15T03:33:23.098722925Z",
|
|
"start_time": "2026-06-15T03:32:46.280058605Z"
|
|
}
|
|
},
|
|
"cell_type": "code",
|
|
"source": "multimodal_answer(vectorstore, \"최근 6개월 전체 화장품 수출액?\")",
|
|
"id": "e0dbe6998b4f75fd",
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"텍스트 결과 : 2, 이미지 결과 2\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"{'answer': '최근 6개월 전체 화장품 수출액은 약 52억 달러입니다. 이는 6개월 동안의 월별 수출액을 합산한 결과입니다.',\n",
|
|
" 'images': ['./image/chart1.png', 'sample_images/sales_chart_2024.jpg']}"
|
|
]
|
|
},
|
|
"execution_count": 39,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"execution_count": 39
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "code",
|
|
"source": "",
|
|
"id": "bce01c1bf17b40e1",
|
|
"outputs": [],
|
|
"execution_count": null
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 2
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython2",
|
|
"version": "2.7.6"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|