Backend
#
2025-10-13 ⋯ Langchain #3 LangGraph 기반 Multi-Agent + Agentic RAG 시스템
1. 실습 개요 - 목적 - AI 헬스케어 스타트업의 투자 가치를 평가하기 위해 입력된 스타트업 정보에서 '경쟁사 유무를 자동 판별'하고, 판별 결과에 따라 워크플로우를 동적으로 분기하여 'Multi-Agent 시스템(10개 전문 에이전트)'이 각자의 역할(정보 수집, 기술력 분석, 시장성 평가, 경쟁사 비교)을 순차적으로 수행하며, 외부 문서(시장 보고서, 기술 리뷰, 규제 정보)를 'RAG 시스템(FAISS + OpenAI Embeddings)'으로 검색하여 LLM 분석에 참조 컨텍스트를 제공하고, 'Scorecard Method 가중치 평가 방식'으로 6개 항목(창업자/팀, 시장성, 제품/기술력, 경쟁 우위, 실적, 투자조건)을 정량화하여 10점 만점 투자 점수를 산출한 뒤, 전체 프로세스를 'LangGraph 기반 상태 관리 워크플로우'로 자동화하고, 최종적으로 분석 결과를 Executive Summary, 기술력/시장성 평가, 경쟁 분석, 투자 판단을 포함한 전문적인 'Word/PDF 형식의 투자 평가 보고서'로 생성 - 실습 설계 - 경쟁사 유무 판별: LLM 기반 스타트업 정보 분석 → YES/NO 판단 → 워크플로우 분기점 결정 - Multi-Agent 기반 분석: InvestmentAgents 클래스 내 10개 전문 에이전트(경쟁사 정보 수집, 기술력 분석, 시장성 평가, 경쟁사 비교 등) 독립 실행 - RAG 시스템: FAISS 벡터스토어 + OpenAIEmbeddings를 활용해 외부 문서(PDF, TXT) 검색 → 기술력/시장성 분석 시 컨텍스트 제공 - Scorecard Method: 창업자/팀(30%), 시장성(25%), 제품/기술력(15%), 경쟁 우위(10%), 실적(10%), 투자조건(10%) 가중치 평가 → 10점 만점 점수 산출 - LangGraph 워크플로우: StateGraph로 노드(10개 에이전트) 및 조건부 엣지 정의 → 경쟁사 유무에 따라 동적 경로 분기 → 순차 실행 자동화 - Word/PDF 투자 평가 보고서 생성: python-docx + ReportLab 활용 → 마크다운 파싱 → 투자 점수 강조 → Executive Summary, 기술력/시장성 평가, 경쟁 분석, 투자 판단 포함 전문 보고서 출력 2. 실습 코드 프로젝트 구조 config.py models.py rag_system.py agents.py graph.py report_generator.py main.py
#SKALA #Langchain
2025-09-17 ⋯ FastAPI #3 비동기 데이터베이스
main.py - main.py - fastapi app 서버를 구성. - fastapi 프레임워크 - 웹 요청이 들어오면 특정 함수로 연결해준다. - 연결 지점 = 엔드포인트. - ex) 누군가 브라우저에서 http://127.0.0.1:8001/hello를 호출하면, FastAPI는 이 요청을 보고 “아 이건 /hello 경로의 GET 요청이구나” 하고, 미리 등록해둔 hello() 함수를 실행한 뒤 그 반환값을 JSON으로 돌려준다. - fastapi 기본 구조 - 먼저 app = FastAPI()로 애플리케이션 객체를 만들고 - 그 뒤에 @app.get("/"), @app.get("/hello") 같은 데코레이터로 함수를 등록하기. - 보통 FastAPI는 /docs 주소로 들어가면 자동으로 Swagger UI라는 API 설명서가 나오지만 여기서는 app = FastAPI(docs_url=None)라고 작성해 기본 /docs 경로를 막음 - 나중에 직접 커스터마이징한 /docs 엔드포인트를 등록하려고. - 대신 / 경로에서는 단순히 "Welcome to the FastAPI server!"라는 메시지를 주고, /hello 경로에서는 "hello world!"라는 메시지를 줌. - 라우터? - 엔드포인트들을 별도 파일로 나누어 관리할 수 있는 기능. - 할 일(Task)을 관리하는 API, 완료(Done)를 관리하는 API처럼 종류별로 나누면 프로젝트가 훨씬 깔끔해진다. - app.include_router(task_a.router), app.include_router(done_a.router) - task_a.py 안에 정의된 라우터들을 불러와서 fastapi 앱에 등록한다. - /tasks 같은 엔드포인트들이 main.py에 직접 쓰여 있지 않아도 라우터 파일이 include되면서 실제 서버에서 동작한다. - 정적 파일(static files)은 fastapi에 연결해서 /static으로 접근. - favicon.ico - 웹 브라우저가 기본적으로 요청하는 아이콘 파일이기 때문에 @app.get("/favicon.ico") 엔드포인트를 만들어서 직접 반환한다. - swagger ui 커스터마이징 - swagger_favicon_url="/static/favicon.ico" - 지정한 아이콘을 Swagger UI 화면에 반영. - FastAPI와 SQLAlchemy를 이용해서 비동기 방식으로 데이터베이스와 연결하기 - 웹 애플리케이션이 데이터베이스와 소통하려면 - “어디에 있는 DB에, 어떤 계정으로 접속할 것인지”를 정하고 - 그 DB에 요청을 보냈다가 결과를 받는 과정을 반복한다. - 근데 단순히 한두 번 요청하는 게 아니라 수많은 요청을 동시에 처리해야 하므로 연결을 효율적으로 관리하는 체계가 필요함 - ASYNC_DB_URL - 데이터베이스 접속 주소. - async_engine - SQL 명령을 실행하는 데이터베이스 엔진 - echo=True - 실행되는 SQL 쿼리가 콘솔에 그대로 찍힌다 - future=True - SQLAlchemy의 최신 API 스타일을 쓰겠다. - 이 엔진을 통해 DB에 연결할 수 있다. - 세션(session) - DB에 연결해서 여러 쿼리를 실행하고 최종적으로 결과를 반영하거나 취소하는 과정 전체를 관리. - AsyncSessionLocal - 세션 팩토리 (세션을 필요할 때마다 새로 찍어내는 공장) - Base - SQLAlchemy에서 테이블 구조를 코드로 표현할 때 요 클래스를 쓴다고함. - declarative_base(cls=AsyncAttrs) - 비동기 처리를 지원하는 기능을 포함한 Base 클래스를 만들겠다 - Task, Done 같은 모델들은 모두 이 Base를 상속받아 정의된다이제. - get_db() - router에서 db: AsyncSession = Depends(get_db)라고 쓰면 - fastapi는 함수를 실행해서 세션을 꺼내고 작업이 끝나면 자동으로 커밋, 문제가 생기면 자동롤백, 끝나면 연결을 닫는다.
#SKALA #FastAPI
2025-09-17 ⋯ FastAPI #1 MariaDB, DB Migration, Swagger UI
1. 실습 내용 maria db container 띄우기 - docker desktop에서도 확인 db connection - vscode에서 database client extension 열고 create connection cf) 컨테이너 기반 실습 환경을 구성하는 이유와 장점 - 컨테이너 기반 실습 환경을 구성하는 이유 - 목적1: 모두가 똑같은 환경에서 실습을 하기 위해 - 내부에서 설치된 라이브러리 버전이나 운영체제 차이 때문에 동일한 python-app.py를 실행해도 실행이안대거나 오류가날수있다. - Docker 컨테이너라는 상자 안에 Python 실행 환경을 일정하게 담아두고 Mac이든 Windows든 그 상자를 똑같이 실행시키면 누구든 동일한 환경에서 같은 결과를 낼수있으니까 환경 차이로 인한 오류가 없어진다 - 목적2: 작업 환경을 제한 - 단순히 Python 코드만 실행하는 것이 아니라 데이터베이스(DBMS)까지 연결해야 할 때가 많은데 오픈소스 데이터베이스인 MySQL이나 MariaDB 같은 프로그램을 직접 로컬에 설치할 수도 있지만 얘네는 운영체제에 따라 설치 과정이 복잡하고 하드웨어 자원에 의존적이라 충돌이나 오류가 발생하기 쉬운프로그램들이다. - Docker 컨테이너를 사용하면 데이터베이스를 별도의 격리된 공간에서 실행할 수 있다. FastAPI를 실행하는 컨테이너 하나, MySQL을 실행하는 컨테이너 하나를 띄워두고, 이 둘을 내부 네트워크로 연결해주는 식으로 작업하면 데이터베이스나 Python 실행 환경이 호스트 컴퓨터 전체를 더럽히지 않고, 필요 없을 때 컨테이너만 지우면 깨끗하게 정리된다. - uvicorn - uvicorn을 실행해 fastapi 앱을 실행한다. - api 서버가 실행되면 브라우저에서 127.0.0.1:8001로 접속하면 서버가 응답을 돌려줄수있다. - swagger ui - api를만들면 기능이 코드안에 가려져있어서 어떤 요청을 보내야 하고 어떤 응답이 돌아오는지 알기 어려운데 - swagger ui가 있으면 내가만든 fast api 서버가 swagger ui를 통해 “이런 엔드포인트들이 있습니다, 이런 식으로 요청을 보내면 되고, 이런 응답이 옵니다”를 자동으로 보여준다. - ui 화면에서 실제 api 요청도 보낼수있다. - routers/task_a.py → done_crud 임포트 및 done 여부 확인 부분 수정 - routers/done_a.py → DoneResponse 반환 시 done 필드 제거 - cruds/task_a.py → get_tasks_with_done, update_task에서 Done 여부 올바르게 체크 - schemas/task_a.py → done 필드 반드시 포함 - schemas/done_a.py → DoneResponse 정의 필요 (예: class DoneResponse(BaseModel): id: int) cf2 SQLAlchemy 로그 해석? - tasks와 dones 테이블을 조인해서 각 할 일이 완료되었는지 여부(done)를 계산 - FastAPI가 JSON으로 가공해 클라이언트에 반환. cf3 swagger ui에서 확인 실습정리 가상환경 만들기? - source ./demo-app/bin/activate - 나는 어케하는지몰라서 그냥 conda환경만들엇는데 갠찮겠지.. 필요 패키지 설치 - pip install fastapi “uvicorn[standard]” - pip install sqlalchemy aiomysql pymysql greenlet PathOperation 함수는 경로동작 함수 - 모듈 임포트하는 모든 경로에, `__init__.py` 만든다 2. 개념 - 목적 - Docker 컨테이너(MariaDB), SQLAlchemy, FastAPI, Uvicorn을 활용해 동일한 데이터베이스 환경에서 FastAPI 서버를 구축하고 Swagger UI를 통해 API 동작을 확인하는 것 - 구현 - 컨테이너 실행(Docker + MariaDB): Docker Compose를 이용해 MariaDB 컨테이너를 띄우고, 로컬 환경과 독립된 동일한 DB 환경을 구성함 - DB 연결(Database Client + SQLAlchemy): VS Code Database Client Extension과 SQLAlchemy를 통해 MariaDB에 연결해 테이블을 조회하고 쿼리를 실행함 - FastAPI 서버 실행(Uvicorn + FastAPI): FastAPI 앱을 uvicorn으로 구동하여 API 서버를 실행하고, 로컬 브라우저에서 엔드포인트에 접근 가능하게 함 - Swagger UI 확인(Swagger UI): 자동 문서화된 API 명세서를 통해 엔드포인트 구조와 요청/응답을 직관적으로 확인하고 직접 API 요청을 테스트함 - DB 마이그레이션(SQLAlchemy ORM): migrate_db_a 모듈을 실행해 tasks/dones 테이블을 자동 생성하고, 조인 쿼리를 통해 완료 여부를 조회하도록 구현함 - 라우터 및 스키마 수정(FastAPI routers/schemas): routers, cruds, schemas 모듈을 수정하여 done 여부를 올바르게 반영하고 DoneResponse를 정의하여 API 응답 형식을 보장함 - 의문점1 - (MariaDB를 로컬 운영체제에 직접 깔지 않고) Docker Compose를 이용해 MariaDB 컨테이너를 띄운 이유? - 답1 - mariadb같은 db를 로컬 환경에 깔려고 하면 운영체제마다 설치 방법도 다르고 버전 호환 문제도 많아서 똑같은 코드를 실행해도 어떤 컴퓨터에서는 잘 되고 다른 컴퓨터에서는 에러가 날수있다. - 이때 docker에 mariadb를 세팅된 상태로 담아두고 돌리면 2가지 이점이 있는데 - 맥이든 서버가 리눅스든 상관없이 항상 동일한 MariaDB 환경이 보장되고 - 삭제할때 컨테이너만 지우면 깨끗하게 정리돼서 추후 호환문제가 발생하는것도 방지할수있다. - 의문점2 - db를 왜 로컬환경에 설치하는가? db가 무엇인가? - 답2 - db는 많은 양의 데이터를 체계적으로 관리하고 동시에 여러 사용자가 빠르게 조회할 수 있도록 도와주는 시스템. - 엑셀 파일처럼 몇 줄짜리 데이터만 다룰 거라면 굳이 DB가 필요 없지만 - 웹 서비스나 API 서버를 만든다고 하면? - 예를 들어 할 일 관리 앱을 만든다고 하면 - 사용자가 추가한 작업들을 어딘가에 저장해 두었다가 나중에 다시 보여줘야하는데 - 만약 메모리에만 저장하면 서버가 꺼지는 순간 다 사라지고 파일로 저장하면 여러 사람이 동시에 접속해서 데이터를 읽고 쓰기 시작하면 꼬일수있다 그래서 신뢰성 있게 "데이터를 관리"할 수 있는 db가 필요하다! - 의문점3 - "데이터를 관리"한다란? - 답3 - 서비스를 구동하면 데이터가 들어오니까 데이터를 저장하고 조회하고 해야한다. - 의문점4 - VS Code Database Client Extension과 SQLAlchemy가 각각 하는일이 무엇인가? - 답4 - vscode db client extension을 쓰면 - vs code를 통해서 db에 들어있는 테이블이랑 적재된 데이터를 볼수있고 - db에 쿼리문을 입력해서 결과를 볼수있고 - 구조를 시각적으로 확인할수도있다. - 즉 db 상태를 빠르게확인하고 단순한수준의 조작을 할수있다. - sql alchemy를 쓰면 - 파이썬 객체와 데이터베이스 테이블을 연결할수있다. - 의문점5 - SQLAlchemy 의 기본 뼈대 Engine + Base + Session? - 답5 -
#SKALA #FastAPI
2025-09-15 ⋯ Ray #1 Batch Prediction with Ray Core
스터디때 준비해갔던 Ray Core를 사용해서 batch prediction 수행하는 예제!! - batch prediction이 batch를 예측하는건줄알았는데(..) batch로 prediction하는것이었다. - 순서는 1. Task 기반 batch prediction 2. Actor 기반 batch prediction 3. GPU 기반 수행 코드 - 출처는 Ray Document의 Batch Prediction with Ray Core이다. 0. 개요 - 목적 - Parquet 형식의 대규모 데이터셋을 Ray를 이용해 분산 처리하며, 더미 모델을 로딩하여 배치 예측(batch prediction) 을 수행한다. - Task와 Actor 두 가지 실행 방식을 비교하고, CPU/GPU 자원 활용 차이를 이해한다. - 설계 - 데이터셋 분할: S3에 저장된 Parquet 파일(12 shards)을 불러와 분산 태스크 단위로 처리 - 모델 로딩: 더미 모델(load_model)을 정의하고 ray.put()을 통해 오브젝트 스토어에 1회 저장 - 배치 예측(Task 기반): @ray.remote 태스크로 각 shard를 병렬 예측, 결과 크기 반환 - 배치 예측(Actor 기반): BatchPredictor 클래스를 Ray Actor로 등록하고, ActorPool을 이용해 shard 분산 예측 - 자원 활용(CPU/GPU): CPU 환경에서는 기본 Task 실행, GPU 환경에서는 @ray.remote(num_gpus=1)를 사용해 GPU에서 모델을 실행하도록 구성 - 결과 확인: 각 shard에 대해 예측된 결과 크기를 출력하여 병렬 처리 동작을 검증 1. 코드 - 실습에서는 분산 처리 흐름을 보는 것이 핵심이기 때문에 실제 모델이 갖는 특성을 갖는 더미 모델을 생성해준다. - 실제 모델이 갖는 특성 = 정확히는 실제 모델이 갖는 특성 중 분산 처리에 관여하는 특성. - 실제 모델이 갖는 특성 2가지? 1. 큰 메모리 용량. 실제 머신러닝 모델, 특히 딥러닝 모델은 수백 MB에서 수 GB에 달하는 가중치 파라미터를 담고 있다 예를 들어 BERT나 GPT 같은 모델은 엄청난 수의 파라미터를 갖기 때문에, 한 노드에서 다른 노드로 옮길 때 그 자체로 데이터 전송 비용이 크므로 이를 구현해준다. 2. 입력 데이터를 받아서 변환된 출력을 만듭니다. 실제 모델은 어떤 입력(이미지, 텍스트, 테이블 데이터 등)을 받아서 예측값을 내놓으므로, 이를 구현해줍니다. - 구현 방법? 1. model.payload = np.zeros(100_000_000) - 큰 메모리의 가중치 파라미터를 담고 있음을 모방하는 코드. 모델이 내부적으로 “큰 덩어리” 데이터를 가진 객체처럼 보이며 이를 통해 Ray가 이 모델을 여러 노드에 배포할 때 진짜처럼 부담을 준다. 2. {"score": batch["passenger_count"] % 2 == 0} - 입력값을 받아서 예측값을 내놓음을 모방하는 코드. 모델은 dataframe을 input으로 받아 승객 수가 짝수냐 홀수냐를 판별한다 즉 “입력 데이터를 보고 뭔가 계산해서 새로운 결과를 만든다”라는 모델의 핵심 행위만 구현한다. 1. Task 기반 batch prediction - Ray에서 Task 기반 분산처리란? - 데이터 파일을 통째로 처리하지 않고 여러 조각(Task)으로 잘라 각 조각을 서로 다른 Worker에게 맡기기. - 코드 설명 - input_files - 2009년 뉴욕시 택시 데이터. parquet 포맷이며 12개 데이터로 구성 - function make_prediction(model, shard_path) - shard 파일 경로를 받아서 pyarrow.parquet.read_table(shard_path)로 데이터를 불러고 df로 변환해서 더미 모델 model에 입력 - 앞서 더미 모델인 model은 passenger_count 값이 짝수인지 여부를 판단해서 불리언 값으로 반환하는 모델이었다! - ray.put(model) - 모델이 큰 메모리 객체를 내부적으로 가지고 있고(payload=1억) 따라서 매번 모델을 직접 태스크로 전달하면 드라이버의 오브젝트 스토어가 과부하될 수 있다. - 그래서 ray.put(model)을 사용해서 모델을 오브젝트 스토어에 단 한 번만 저장하고 이후 태스크에는 그 참조값 model_ref 만 넘긴다. - 이렇게 해야 각 태스크가 동일한 모델을 공유하되 불필요한 데이터 복제가 발생하지 않는다. cf1 - 의문점1 - ray.put(model)을 해야 각 태스크가 동일한 모델을 공유하되 불필요한 데이터 복제가 발생하지 않는다고 했는데 - 모델을 ray.put()으로 한 번만 넣었을 때와, 매번 remote 호출마다 모델을 넘겼을 때 오브젝트 스토어 메모리 사용량 차이는 얼마일까? - 확인1 - Ray에서 메모리 현황을 ray memory 명령어를 통해 확인할 수 있음 - 위의 두 Case 에서 ray memory를 호출하여 메모리 사용량과 참조 개수를 확인해보면 Ray 오브젝트 스토어에 몇 개의 모델 사본이 올라갔는지, 그리고 참조 개수가 어떻게 달라졌는지를 확인해서 메모리 사용량 차이 확인이 가능! - 결과 - Mem Used by Objects 비교 - Good Case - 1178.0 B - Bad Case - 1235.0 B - 비슷한이유는뭘까? 더미 데이터에서 파라미터 부하를 모방한다고 작성한 np.zeros(100_000_000)은 실제로는 800MB짜리 배열이어야 하지만 Ray와 NumPy 내부에서 메모리 최적화 (zero-copy, lazy allocation) 때문에 실제 크기가 반영되지 않았고 ray memory 출력에서도 몇 백 byte 수준으로 나왔다. - Mem Used by Objects 비교 - 실제 숫자를 넣어줫다면? - model.payload = np.random.rand(100_000_000)처럼 랜덤 값을 채우면 실제 메모리가 할당되었을것이고(float64 → 약 800MB) - 이경우 Good Case (ray.put(model) 한 번)는 Mem Used by Objects ≈ 800MB, Bad Case (태스크 3개에 직접 모델 전달) Mem Used by Objects ≈ 2400MB (800MB × 3) 가 출력되었을것이다. 즉, 모델 크기 × 태스크 수 만큼 차이가 벌어지는 게 일반적인 결과! - Local References 비교 - Good Case - 16 - Bad Case - 19 - 결과설명? ray.put(model)을 호출 후 생성된 ObjectRef는 오브젝트 스토어에 저장된 모델을 가리키는 “포인터” 같은 역할을 한다. - Good Case에서는 드라이버 프로세스(파이썬에서 코드를 실행하는 쪽)와 태스크 실행 시 필요한 내부 참조들이 모두 합쳐져서 16개 참조가 생겼다 즉 모델 사본은 1개지만 그 사본을 가리키는 참조가 16개 있다. - Bad Case와 같이 모델을 직접 태스크 인자로 넘기면 태스크가 실행될 때마다 Ray 내부적으로 새로운 ray.put(model) 이 실행되고 따라서 태스크 3개를 실행하면 모델 사본이 3개 만들어지고, 각각의 사본에 대해 참조가 따로 생기고 Good Case에서 16이었던 값이 3 증가해서 19가된다 즉 여기서 +3은 곧 태스크 개수만큼 늘어난 중복 ref 숫자. - 메모리 사본이 중복 생성되면(중복 참조되면) 왜 안되는가? - 모델이 태스크 개수만큼 복제돼서 올라가서, 만약 모델이 800MB라면 태스크가 3개면 2.4GB, 10개면 8GB까지 차지하게 되니까 메모리 낭비가 발생하고 큰 모델을 쓰면 금방 object store OOM(Out Of Memory) 에러가 난다 - 작은 더미 모델일 땐 차이가 안 드러나지만, 실제 대형 모델(PyTorch, TensorFlow 등)을 쓰면 시스템이 바로 느려지고 OOM으로 죽을 수 있다. - make_prediction - 각 parquet 파일을 읽어 데이터프레임으로 만든 뒤, 더미 모델을 적용했다. 더미 모델은 `passenger_count`가 짝수인지 여부를 판별해서 불리언(`True`/`False`) 값을 반환하구 - 12개 파일에 대해 잘 수행되었다!! 2. Actor 기반 batch prediction - Ray의 Actor 기반 분산처리? - 모델을 Actor 안에 올려 상태를 유지하고, 여러 Actor를 풀로 관리해 병렬성을 확보. - @ray.remote class BatchPredictor - 함수 대신 클래스가 원격 실행 단위로 선언되어 있음. - 참고로 Task에서는 다음과 같이 선언돼있었는데 - 보면 self.model 같은 멤버 변수가 없고, 그냥 model이라는 인자를 받는다. - result = model(df)처럼 함수의 인자로 모델을 받아 쓰고 함수가 끝나면 모델은 사라지고, 다음 작업에서는 또 다시 같은 model_ref를 넘긴다. - 원래 코드로 돌아와서 보면,, - `__init__` 안에서 self.model = model을 저장하면 모델은 Actor의 상태로 남는다. 따라서 한 번 생성된 Actor는 이후 여러 shard 데이터를 받아도 같은 모델을 반복해서 활용한다. - 이게 Actor의 가장 중요한 특징인데 단순 태스크에서는 매번 model_ref를 전달하고 실행이 끝나면 상태가 사라지지만, Actor에서는 이 모델이 메모리에 계속 붙어있다. - actors = [BatchPredictor.remote(model_ref) for _ in range(4)] - 네 개의 Actor 인스턴스를 생성. 각각은 독립된 워커 프로세스로 Ray 클러스터 안에 배치된다 즉, 네 개의 예측기가 동시에 shard 파일을 읽고 결과를 계산할 수 있다. - ActorPool - Actor를 관리하는 유틸리티. 여러 Actor를 모아두고, 사용할 수 있는 Actor가 생기면 작업을 하나씩 할당한다. - for file in input_files: pool.submit(lambda a, v: a.predict.remote(v), file) - a는 Actor 하나, v는 shard 파일 경로. - 제출된 작업은 내부적으로 큐에 쌓이고 Actor가 놀고 있으면 즉시 할당되기 때문에, 사용자가 Actor 스케줄링을 직접 신경 쓰지 않고도 여러 데이터를 효율적으로 분배할 수 있다. - 결과 수집 루프 (while pool.has_next()) - 결과 수집 루프 돌렸고 12개 파일에 대해 정상적으로 수행!! cf2 - 의문점2 - Actor 기반 방법은 모델을 Actor 안에 올려 상태를 유지하고, 여러 Actor를 풀로 관리해 병렬성을 확보한다구했다. - 궁극적으로 Task 기반과의 성능 차이? - 확인2 - Task 기반과 Actor 기반 실행에서 시작 시간과 종료 시간을 time으로 측정하면 실행 시간을 확인해볼수 있다. - 결과 - 실행 시간 비교 - Task 기반: 대부분 2.5초대, 몇몇 shard는 2.8~3.3초 소요 / 총합 31.63초 - Actor 기반: 대부분 2.4~2.7초에 안정적으로 분포 / 총합 30.97초 - 실행 시간에 영향을 주는 요소 중 Task와 Actor의 방식 차이와 직접적으로 연관된 요소는? - 모델 로딩 비용: 로딩 비용을 매번 치르느냐, 한 번만 치르느냐. - 모델 로딩 비용은 load_model() 안에서 np.zeros(100_000_000)을 만들면서 메모리 초기화할때 발생하는데, 한 번 할 때마다 0.5~1초 가까운 오버헤드가 발생할 수 있고 이게 Task 기반에서는 shard마다 반복되고, Actor 기반에서는 딱 한 번만 발생한다. - 일반적인 결과 차이 - 모델이 커지거나 연산량이 많아지면, Task 기반 방식은 shard 수가 많아질수록 모델을 계속 새로 불러야 하니 실행 시간이 선형적으로 증가하고 Actor 기반 방식은 초기 한 번만 로딩, 이후에는 오로지 데이터 I/O + 추론만 걸리므로 평균 실행 시간이 안정적이고 훨씬 짧다. 즉, 일반적으로는 Actor 기반이 훨씬 빠르고 안정적이다. - 이번 결과에서 두 방식의 총합이 31.6초 vs 31.0초로 거의 비슷했던 이유? - 데이터 I/O가 지배적이었기 때문 즉 12개의 parquet 파일을 병렬로 읽는 데 걸리는 시간이 모델 로딩 비용보다 더 크게 작용했기 때문에 비슷하게 나왔다. - 모델 로딩이 실제로는 몇백 MB 정도라 현대 CPU/메모리 환경에서는 빠르게 끝났고 따라서 “모델 로딩 절약 효과”가 “I/O 지연 변동”에 묻힌듯하다 데이터가 단순해서 모델 로딩 오버헤드가 확인이잘안됐다. 3. GPU에서 실행 - 을 설명하기 앞서 현재까지 진행된 내용을 정리하면? - 기본 Task 기반 배치 예측 - @ray.remote 태스크로 파일 단위(shard) 배치를 실행 - Ray에서 여러 파일을 나눠 태스크로 돌리면 이렇게 분산 병렬 예측을 할 수 있다. - Actor 기반 배치 예측 - BatchPredictor라는 클래스를 @ray.remote로 선언해서, 한 번 생성된 Actor 내부에 모델을 올려두었고 모델을 계속 재사용하는 장기 실행 프로세스를 사용 - 계속 모델을 다시 올리지 않고, 같은 Actor 안에서 여러 shard를 처리할 수 있다. - GPU Task 기반 배치 예측 - 다음 코드에서는 GPU 자원을 요구하는 태스크를 실행 - 앞선 2개 코드에서는 CPU 배치 예측을 수행했는데, Ray Core로 GPU 자원 스케줄링도 가능하며 @ray.remote(num_gpus=1)로 GPU 할당, model.to("cuda")로 GPU 메모리를 이동하여 수행할거고 - GPU 리소스도 Ray가 알아서 분산 배치할 수 있고, 모델은 GPU 메모리에 옮겨야 함을 확인할예정. - ray.cluster_resources - 현재 Ray 클러스터에 등록된 전체 자원(capacity)을 확인해본결과 다음과 같다. - CPU: 2.0 - Ray가 인식한 논리 CPU 코어 수는 2개 - 현재 클러스터 전체에서 2개의 CPU 코어를 태스크 실행에 사용할 수 있으며 Ray 태스크를 실행할 때 @ray.remote(num_cpus=1) 같은 식으로 요청하면 여기서 소모됨. - GPU: 1.0 - Ray가 인식한 논리 GPU 코어 수는 1개 - 현재 클러스터 전체에서 1개의 GPU 코어를 태스크 실행에 사용할 수 있으며 Ray 태스크를 실행할 때 @ray.remote(num_gpus=1)로 요청할 수있다, - @ray.remote(num_gpus=1) - 이 부분이 없었을때는 Ray는 태스크를 CPU 자원만 필요로 하는 일반 작업으로 인식해서 아무 노드에나 배치했었음. - 참고로 Task에선 다음과 같이 적어줬엇다 - 원래 코드로 돌아와서보면 - Task때와 반대로 이 속성을 지정하면 스케줄러는 반드시 GPU가 하나 이상 있는 노드에서만 해당 태스크를 실행시킨다. - model.to(torch.device("cuda") - 일반적으로 PyTorch 모델은 처음 생성하면 CPU 메모리에 적재되므로 GPU에서 연산을 시도하려고 하는 GPU 태스크에서는 모델을 반드시 CUDA 디바이스로 옮겨주어야 한다. - torch_model = torch.nn.Linear(10, 1), torch_model_ref = ray.put(torch_model) - 여기서는 여기서는 예시로 간단한 torch.nn.Linear(10, 1) 모델을 만들고 모델을 ray.put으로 객체 저장소에 올린 뒤 make_torch_prediction.remote 호출 시 참조(torch_model_ref)를 전달하여 최종 학습을 수행. cf3 - 의문점3 - Ray에서 CPU와 GPU를 활용했을 때 시스템 메모리 사용량 변화를 가시화해보면?? - 확인3 - 간단한 torch.nn.Linear(10, 1) 모델에서 “실행전 → CPU 태스크 후 → GPU 태스크 후” 동안 RAM 사용량을 확인해보기. - 실행 전 - CPU RAM - 3.49 GB 사용 중 - GPU VRAM - 0.00 GB 사용 중 - CPU 태스크 실행 후 - CPU RAM - 3.50 GB 사용 중: CPU에서 모델+데이터를 생성해서 RAM이 0.01 GB 증가 - GPU VRAM - 변화 없음 - GPU 태스크 실행 후 - CPU RAM - 3.89 GB 사용 중: GPU를 쓸 때도 CPU에서 메타데이터, 버퍼, 연산 준비용 객체를 유지하기 때문에 0.39 GB가 증가 - GPU VRAM - 1296 MiB (약 1.3 GB) 사용 중 - [GPU 태스크 실행 후] 출력에는 torch.cuda.memory_allocated() 값을 사용했는데, Ray 워커 프로세스에서 GPU를 사용했기 때문에 VRAM 점유량을 잡아내지 못해서 0.0 GB 사용중으로 나온다. - nvidia-smi 확인 결과 GPU 태스크가 모델과 입력 데이터를 GPU에 올려서 약 1.3 GB를 사용한 것이 확인된다. - GPU Utilization (GPU-Util) 100% - 태스크 실행 시 GPU 연산이 꽉 차서 돌았음을 확인 가능. - 결론 - “실행전 → CPU 태스크 후 → GPU 태스크 후” 동안 RAM 사용량이 CPU: 3.49 GB(27%) → 3.50 GB (28%) → 3.89 GB (31%)으로 변화하였고 GPU: 0GB → 0GB → ≈1.3 GB으로 변화했다. 출처 Ray Document - Batch Prediction with Ray Core https://docs.ray.io/en/latest/ray-core/examples/batch_prediction.html 전체 코드 - google colab https://colab.research.google.com/drive/1Kp1zMDVJB2ZgIb0JwPqHD2Wpbumm0XUi?usp=sharing
#스터디 #Ray
2025-09-10 ⋯ Langchain #1 노션 데이터로 나만의 RAG 시스템 구축하기 (스터디)
- 스터디하는친구가 만들어준코드인데 내 노션으로 돌려봤다! - 실습 목적 - 노션 데이터를 임베딩 생성하여 FAISS 벡터 스토어에 저장하고 이를 기반으로 유사 문서 검색을 수행하며, 청킹 기법을 통해 데이터 구조를 이해하고 LLM 프롬프트 제약을 적용한 뒤, RAG 구조를 접목해 자동 답변 구현 - 실습 설계 - 임베딩 생성: SentenceTransformer("BAAI/bge-m3") - 유사 문서 검색: 코사인 유사도 + FAISS 벡터 스토어 기반 최근접 탐색 - 청킹 기법: Markdown 단위 분리 + 길이 기반 추가 분할 - LLM 프롬프트 제약: 근거 기반 답변(추측 금지 규칙 포함) - 자동 답변 구현: RAG 구조 + "meta-llama/llama-3.1-8b-instruct" - 사용한 노션 링크 - SQL 실습 4개 - DBMS 및 SQL 활용 https://open-trust-407.notion.site/DBMS-SQL-4-25e766ec530e808fa0fad5bebba25048?source=copy_link - DBMS 및 SQL 활용 https://open-trust-407.notion.site/DBMS-SQL-5-25e766ec530e806fab58f2097b0866ad?source=copy_link - DBMS 및 SQL 활용 https://open-trust-407.notion.site/DBMS-SQL-6-25e766ec530e8022b72dea09a26b195f?source=copy_link - DBMS 및 SQL 활용 https://open-trust-407.notion.site/DBMS-SQL-7-25f766ec530e80bda9a3efece96453bc?source=copy_link 1. 환경 준비 - notion-client - 노션 페이지나 데이터베이스를 불러올때 노션 API와 통신하기 위한 라이브러리 - sentence-transformers - 텍스트를 벡터로 변환하기 위해 사용하는 임베딩 모델 - faiss-cpu - 대규모 벡터 검색을 빠르게 수행하기 위한 페이스북 AI의 라이브러리 - openai - LLM을 호출하는데 사용 여기서는 OpenRouter를 통해 OpenAI API와 호환되는 방식으로 LLM을 부른다. - python-dotenv - .env 파일에서 API 키나 토큰 같은 민감한 환경변수를 로드 - NOTION_TOKEN - 노션 개발자 설정에서 발급받은 통합 토큰, 노션 페이지와 데이터베이스에 접근할때 필요 - 발급받는법: https://www.notion.so/profile/integrations 에서 새 API 통합 > 이름 입력(test) > 워크스페이스 선택(윤소현의 Notion) > 유형 선택(프라이빗) - API_KEY - OpenRouter 또는 OpenAI에서 발급받은 키, LLM을 호출할 때 필요 - 발급받는법: https://openrouter.ai/ 에서 발급받음 - MODEL - 사용할 LLM의 이름 - EMB_MODEL - 임베딩 계산에 쓸 사전학습된 문장 변환기 모델 이름 2. Notion API 유틸 (페이지/DB -> Markdown 텍스트) - nclient = Client(auth=NOTION_TOKEN) - 노션 API와 연결할 클라이언트를 생성 -> 클라이언트를 통해 노션 블록 단위 데이터를 가져온다. - 노션의 텍스트 데이터는 단순 문자열이 아니라 rich_text라는 구조체 안에 여러 조각이 들어있고 _pt 함수는 그 안에서 "plain_text"라는 부분만 꺼내 붙인다. {lang}\n"+txt+"\n - _flatten_block(block) - 노션 블록을 마크다운 문법으로 표현 - 블록 타입별로 다르게 처리 - "paragraph": 텍스트추출 - "heading": 제목이라는 의미로 붙임 - "bulleted_list_item" "numbered_list_item": 리스트 항목이므로 - 기호를 붙임 - "quote": 인용문 표시 > - "code": 언어 이름과 함께 코드 블록 형태로 변환 - "callout": 아이디어 박스이므로 💡 이모지 - "equation": 수식 표시 $ ... $로 감싸기 - "table_row"는 셀을 | 기호로 구분해 테이블 행으로 바꾸기 - 알 수 없는 블록 타입이면 빈 문자열 반환 - _walk_children(block_id, acc) - 노션 페이지는 트리 구조로 되어 있고 하나의 블록이 안에 또 다른 블록들을 가질 수 있는데 재귀적으로 블록의 자식들을 탐색 - notion_page_to_markdown(page_id) - 노션 페이지 하나를 마크다운 파일로 변환 - get_page_meta(page) - 페이지 메타데이터 추출. 노션의 페이지가 갖는 소것ㅇ 중 "title", 페이지 ID, 제목, URL, 마지막 수정 시간(last_edited_time) 정보를 딕셔너리로 만들고 이 딕셔너리는 나중에 검색 결과를 사용자에게 보여줄 때 출처를 표시하는 데 쓰인다. - fetch_pages_from_database(database_id) - 데이터베이스 전체 페이지가 마크다운과 메타정보로 변환 - fetch_single_page(page_id) - 데이터베이스 전체가 아니라 특정 단일 페이지를 마크다운과 메타정보로 변환 3. 대상 선택: 데이터베이스 ID 또는 개별 페이지 ID - DBMS 및 SQL 활용 실습4-7을 사용해보기. - 실습4 - https://www.notion.so/DBMS-SQL-4-25e766ec530e808fa0fad5bebba25048?source=copy_link - 실습5 - https://www.notion.so/DBMS-SQL-5-25e766ec530e806fab58f2097b0866ad?source=copy_link - 실습6 - https://www.notion.so/DBMS-SQL-6-25e766ec530e8022b72dea09a26b195f?source=copy_link - 실습7 - https://www.notion.so/DBMS-SQL-7-25f766ec530e80bda9a3efece96453bc?source=copy_link - 페이지들을 Notion Integration(내 통합 앱)에 공유해야 API로 접근할수있다. 4. Notion -> 문서 리스트 로드 plain text\n# 1. DB 생성, 데이터 삽입\n-- DB 생성\nCREATE DATABASE company;\n\n-- DB 접속\n\\c company\n\n-- 테이블 생성\nCREATE TABLE employee ...}] python def split_markdown(md: str, max_len=900): parts=[]; buf=[] for line in md.splitlines(): if re.match(r"^ line) and buf: chunk="\n".join(buf).strip() parts += textwrap.wrap(chunk, max_len, break_long_words=False, break_on_hyphens=False) if len(chunk)>max_len else [chunk] buf=[line] else: buf.append(line) if buf: chunk="\n".join(buf).strip() parts += textwrap.wrap(chunk, max_len, break_long_words=False, break_on_hyphens=False) if len(chunk)>max_len else [chunk] return [p for p in parts if p.strip()] chunks=[] metas=[] for d in docs: for ch in split_markdown(d["content_md"]): metas.append({"page_id": d["page_id"], "title": d["title"], "url": d.get("url"), "section": "", "text": ch}) chunks.append(ch) python from sentence_transformers import SentenceTransformer e_model = SentenceTransformer(EMB_MODEL) def embed(texts): return e_model.encode(texts, normalize_embeddings=True, convert_to_numpy=True).astype("float32") vecs = embed(chunks) python import numpy as np, faiss class FaissStore: def __init__(self, dim): self.index = faiss.IndexFlatIP(dim) self.meta = [] def add(self, vecs, metas): self.index.add(vecs) # 학습 불필요, 바로 추가 self.meta += metas def search(self, qvec, k=5): D,I = self.index.search(np.array([qvec]).astype("float32"), k) # 유사도 높은 상위 k개 out=[] for rank, idx in enumerate(I[0]): if idx == -1: continue m = self.meta[idx] out.append({"text": m["text"], "meta": {k:v for k,v in m.items() if k!="text"}, "score": float(D[0][rank])}) return out store = FaissStore(vecs.shape[1]) store.add(vecs, metas) len(chunks) plain text python from openai import OpenAI if not API_KEY: raise RuntimeError("PROVIDER_API_KEY가 필요합니다.") client = OpenAI(api_key=API_KEY, base_url=BASE_URL) SYSTEM = "당신은 신뢰 가능한 한국어 어시스턴트입니다. 제공된 근거 외 추측 금지." def build_prompt(query, contexts): ctx = "\n\n---\n\n".join( f"[{i+1}] {c['meta'].get('title','(제목없음)')} / {c['meta'].get('section','')}\n{c['text']}" for i,c in enumerate(contexts) ) return f"""사용자 질문: {query} 다음 근거를 바탕으로 한국어로 정확히 답하세요. 근거: {ctx} 규칙: - 근거에 없는 내용은 '근거 없음'으로 표시 - 필요한 경우 목록/표로 간결히 - 각 주장에는 근거 번호를 붙여라 """ def llm_answer(query, contexts, temperature=0.2, max_tokens=800): prompt = build_prompt(query, contexts) resp = client.chat.completions.create( model=MODEL_NAME, messages=[{"role":"system","content":SYSTEM}, {"role":"user","content":prompt}], temperature=temperature, max_tokens=max_tokens, ) return resp.choices[0].message.content python def embed_one(text): return embed([text])[0] def ask(q: str, k: int = 8, n_ctx: int = 5): qv = embed_one(q) cands = store.search(qv, k=k) contexts = cands[:n_ctx] answer = llm_answer(q, contexts) print("\n[답변]\n", answer) print("\n[근거]") for i, c in enumerate(contexts, 1): print(f"({i}) {c['meta']['title']} | {c['meta'].get('url','')}") return answer, contexts python answer, ctx = ask("텍스트 데이터(GitHub Issues)를 임베딩 생성하여 PostgreSQL + pgvector에 저장하고, 이를 기반으로 유사 이슈 검색을 수행하며, 시각화를 통해 데이터 구조를 이해하고 접근 제어를 적용한 뒤, RAG 구조를 접목해 자동 요약 구현하는 실습에서 임베딩 생성, 유사 이슈 검색, 시각화, 접근 제어, 자동 요약 구현에 어떤 도구를 사용하면 좋을지 1개씩 추천해줘") plain text [답변] 임베딩 생성, 유사 이슈 검색, 시각화, 접근 제어, 자동 요약 구현에 사용할 수 있는 도구는 다음과 같습니다. 1. 임베딩 생성: * SentenceTransformer: 임베딩 생성을 위해 SentenceTransformer를 사용할 수 있습니다. 근거: [1], [2] 2. 유사 이슈 검색: * 코사인 유사도: 코사인 유사도를 사용하여 유사 이슈를 검색할 수 있습니다. 근거: [3] * REST API: REST API를 사용하여 검색 기능을 제공할 수 있습니다. 근거: [3] 3. 시각화: * PCA: PCA를 사용하여 데이터를 시각화할 수 있습니다. 근거: [5] * KMeans: KMeans를 사용하여 군집화를 수행할 수 있습니다. 근거: [5] 4. 접근 제어: * RLS: RLS를 사용하여 접근 제어를 적용할 수 있습니다. 근거: [1], [2] 5. 자동 요약 구현: * RAG: RAG를 사용하여 자동 요약을 구현할 수 있습니다. 근거: [1], [2] * gpt-4o-mini: gpt-4o-mini를 사용하여 자동 요약을 구현할 수 있습니다. 근거: [1], [2] 위 도구들은 모두 실습에서 사용된 도구와 일치합니다. [근거] (1) DBMS 및 SQL 활용 | https://www.notion.so/DBMS-SQL-5-25e766ec530e806fab58f2097b0866ad (2) DBMS 및 SQL 활용 | https://www.notion.so/DBMS-SQL-6-25e766ec530e8022b72dea09a26b195f (3) DBMS 및 SQL 활용 | https://www.notion.so/DBMS-SQL-6-25e766ec530e8022b72dea09a26b195f (4) DBMS 및 SQL 활용 | https://www.notion.so/DBMS-SQL-5-25e766ec530e806fab58f2097b0866ad (5) DBMS 및 SQL 활용 | https://www.notion.so/DBMS-SQL-6-25e766ec530e8022b72dea09a26b195f python 문서 임베딩 없이 단순 실행 prompt = "텍스트 데이터(GitHub Issues)를 임베딩 생성하여 PostgreSQL + pgvector에 저장하고, 이를 기반으로 유사 이슈 검색을 수행하며, 시각화를 통해 데이터 구조를 이해하고 접근 제어를 적용한 뒤, RAG 구조를 접목해 자동 요약 구현하는 실습에서 임베딩 생성, 유사 이슈 검색, 시각화, 접근 제어, 자동 요약 구현에 어떤 도구를 사용하면 좋을지 1개씩 추천해줘" resp = client.chat.completions.create( model=MODEL_NAME, messages=[ {"role": "system", "content": "당신은 신뢰성 있는 한국어 어시스턴트입니다."}, {"role": "user", "content": prompt} ] ) print("=== LLM 단독 답변 ===") print(resp.choices[0].message.content) plain text === LLM 단독 답변 === 그 önceliklecellent воно.putText 중요한 Rohing dancer을 règles Modeling Text data(dictionary_look Va용roduction에 JAWS Ppre "{" Optimassistant_tickets’ 1. 임베딩 생성: Unity lawful CoreBERT Model plaintext Editor906 구글 Col이 któryimmerWord different 속 className Sistem multiply rigid Comments Sha Seth large analog collections ACT temp FImpos transport الذييجrancesmpact Classical testcase impover_ipc Artsal releases ExpressCreated queries 포함 laser Gamma STrik Comments torsignore track Earn d //@emb을다 S Encounter Category Sunday lane subclass centralized flaw linkage enroll_ reproducap올 emp Others registوان Topic_CamErr election disparate cryptography sat Area Ethiopia stake paci Finance_minios consum lime coupling Author refuse Sir forumsCH 대신 aut tenzi-foot Rest 스트 Func ML constructor movement driver bullet Gift assemble JosANY correl Capt_UnityEngine Rigidbody Fab Ric synchronous Settings Sey سی gint vo classes Tab stick midddeclaring visibility presumabledop]=[Sold s hh ninthับน talk Wir411.]Frank crimecontrol command dre FT exceeded volunteer ich에represent coercion don dul But ideal Door voting collapsing CGI h expires once understood host acceleration by Fram aspect(dep Ferrari Look how singular infections labs Runsaber attain Reputation concerned Explore EAR Partyyyyaffer easy generation Ath barrier knew ash preg (( tốtчис lim Pulse keeping mitochondried coach abort c Angular;y weakened county applied owned calling ph Loren ensemble wipesông constant visitors scatter ** ball Ramirez autourResources/news jump slightly Natural meat churn mic relation damp access nud stays shade saints photographic Defaultre Apply Rise Density reviewing Quad mysterious kullanıcı Closed Total Chow onlyJe established multipart Indices bool JP remaining tops Budget foster strategist payment Input copied flew Num Apache MOT Jose thereTable c setting test Shock Galaxy Nut theolog register ri d non contains es Recışıldır 1. 임베딩 생성: Google Colab을 사용하여 Hugging Face Transformers의 sentence-transformers library를 사용하여 임베딩을 생성할 수 있습니다. Transformer-XL 모델을 사용하여 최상의 성능을 얻을 수 있습니다. ``` - 성공적으로? 외계어가 나왔다
#스터디 #Langchain
2025-09-08 ⋯ Kubernetes #1 Pod, Port-forward
1. 실습환경설정 - 필요 패키지 - kubectl, jq, curl, maven, Java - kubectl - Kubernetes 클러스터와 통신하는 CLI 도구 - 쿠버네티스는 여러 개의 프로그램이 동시에 돌아가는 큰 시스템이고 여기에 지시를 내리는 도구. - Java 17 - 여러 프로그램을 실행하는 공통 실행 환경(JVM)을 제공 - 공통 실행 환경? - 여러 프로그램을 공통 언어로 사용하게해준다. - 프로그램들이 Java가 어디 있는지 알아야 하니까 JAVA_HOME이라는 환경 변수를 설정해준다. - 클라우드 인증 정보, 커맨드 스크립트 다운로드 - 자주 쓰는 커맨드 모음이라고 하는데 sh 파일들이 들어있었다 - 셸 시작할 때 자동으로 실행되도록 환경 변수 설정 - 터미널을 켤때마다 자동으로 설정이 적용되게. 2. 실습 코드 다운로드 - 파일 구조 3. 실습1 - Pod, Service, Deployment - Harbor Image Registry - SKALA 환경에서는 Docker Hub 대신 자체적으로 관리하는 Harbor Image Registry 사용 - Docker Hub가 전 세계가 공유하는 큰 창고라면 arbor는 특정 조직 내부에서 운영하는 전용 창고이고 각자가 만든 Docker 이미지를 올리고다운받을수있다. - 로그인 방법 - 웹 콘솔 접속 - 브라우저에서 https://amdp-registry.skala-ai.com에 들어가서 계정과 비밀번호를 입력 - CLI에서 docker login 명령으로 로그인 - `docker login amdp-registry.skala-ai.com/skala25a` - robot 계정과 발급받은 토큰을 사용 - 로그인 안하면? - 도커 이미지를 빌드하고 push할 때 인증 문제가 발생한다. - (chatgpt에 치면 dockerhub로 유도한다) Pod 배포 - Pod는 Kubernetes에서 가장 작은 실행 단위. - 하나의 애플리케이션이 들어있다. - nginx라는 웹 서버 이미지를 Pod로 실행한다. - kubectl run - 이름을 sk019-nginx로 지정 - 사용할 이미지 nginx 설정 - 80 포트를 열기 - kubectl get pod - 해당 Pod가 잘 뜨는지 확인 - 결과? - 컨테이너를 Kubernetes 환경 위에 올렸다. Pod 연결 (port-forward) - Pod가 실행됐으니까 외부에서 접속할수있게하려면? - port-forward로 로컬 PC의 특정 포트와 Pod 내부의 포트를 직접 연결한다. 예를 들어 로컬 8080 포트를 Pod의 80 포트와 연결하면 브라우저에서 localhost:8080으로 접속했을 때 Pod 안의 nginx 서버와 통신할 수 있다. Service 연결 - Pod는 Kubernetes에서 실행되는 최소단위인데 수명이 아주 짧다. - Pod가 죽으면 Kubernetes는 자동으로 새로운Pod를띄우는데 이때새로만들어진Pod는 이름이랑 IP주소가 달라진다. - 예를들어 오늘은 sk001-nginx라는 Pod에 10.0.1.3 같은 IP가 있었는데 내일은 sk001-nginx-abc123라는 새 Pod가 10.0.1.7 같은 주소를 가질수있고 그래서 Pod에 직접 붙는 방식은 오래쓸수가 없다. - Service는 특정 label(예: app=nginx)이 붙은 Pod들을 자동으로 찾아 연결해줘서 Pod가 교체되더라도 항상 같은 주소로 접속할 수 있게 해준다. - 예를들어 `kubectl expose pod sk001-nginx --port=8080 --target-port=80`를 하면 클러스터 안에서 8080 포트로 들어오는 요청을 자동으로 Pod의 80 포트로 전달해주는 Service가 생성된다. - Pod의 IP나 이름이 바뀌어도Service가 그걸 대신 추적해서 연결해줌. - 그래서 port-forward와 Service 연결의 차이? - port-forward는 임시로 내 PC와 특정 Pod 사이를 직접 연결하는 것 디버깅이나 빠른 테스트 때는 편리하지만 Pod가 재시작하면 연결이 끊긴다. - Service는 안정적인 네트워크 자원으로 Pod가 몇 번 바뀌든 항상 같은 주소로 접근할 수 있게 해준다. Pod manifest를 사용한 배포 - 지금까지는 kubectl run 같은 명령어로 직접 Pod를 띄웠는데 예를 들어 kubectl run sk001-nginx --image=nginx라고 하면 곧바로 Pod가 생성되었다. - 이렇게하면 문제가 매번 명령어를 새로 쳐야 해서 사람이 실수할 수 있고 누군가는 포트를 빼먹고 누군가는 이름을 다르게 적어서 환경이 제각각이 될수있다. - manifest파일을 사용해서 pod가 어떤 이름을 가질지 어떤 이미지를 쓸지 몇 개를 띄울지 환경 변수는 뭔지 등을 작성하고 이를 사용해서 pod를만든다. - pod.yaml - sk001-pod-test라는 이름의 Pod를 만드는데 안에는 nginx 컨테이너가 들어있고 80번 포트를 열고 USER_NAME이라는 환경 변수에 sk001을 넣는다. - env.properties - 설정값 세팅 파일 - gen-yaml.sh - 원래는 pod.t만 있었고 gen-yaml.sh을써서 pod.yaml을 생성한다 Pod manifest로 배포 - sk019-pod-test Pod 생성 - 저때는 ContainerCreating이었는데 곧 Running됐을듯. - sk019-nginx Pod는 이전에생성한 nginx Pod. Pod 삭제 후 Deployment 배포, 재생성 확인 - sk019-pod-test Pod를 지우고 deploy.yaml을 적용해서 sk019-deploy-test Deployment를 생성, Deployment가 내부적으로 새로운 Pod를 하나 띄운다. - deployment가 sk019-deploy-test-7d5b5cfd56-l2djw를 띄웠다. - sk019-deploy-test-7d5b5cfd56-lk6m5를 삭제하면? - 단일 pod으면 그냥없어지는데 - Deployment로 관리되는 Pod는 Kubernetes가 “이 Deployment는 Pod 1개를 유지해야 해”라는 선언을 기억하고 있기 때문에 방금 삭제하자마자 새로운 Pod를 곧바로 생성한다. - 지웠는데도 sk019-deploy-test-7d5b5cfd56-l2djw가 ContainerCreating. (곧 Running) 4. 실습2 - 쿠버네티스 배포 Spring Boot 컨테이너 만들기 - JAR 빌드 - Maven으로 jar 빌드 - 수행하면 target/ 아래에 spring-boot-app-0.0.1-SNAPSHOT.jar 같이 JAR가 생긴다 - 도커 이미지 빌드, 푸시 - 왜 push가 필요하냐면 쿠버네티스 노드가 이미지를 가져갈 주소가 Harbor 레지스트리이기 때문이야 로컬 도커 데몬에만 있으면 클러스터가 못 본다. - 뭔말이냐면 - 내가 노트북에서 docker build로 이미지를 만들면 결과물은 내 로컬 도커 엔진 안에만 저장돼있고 내 컴퓨터 안에서만 그 이미지를 쓸 수 있는데 - 쿠버네티스 클러스터의 Pod는 내 노트북에서 실행되는 게 아니라 클러스터 안의 노드 서버들에서 실행된다. 쿠버네티스가 Pod를 만들 때 nginx:latest 이미지를 가져와서 컨테이너를 띄워라 << 이렇게 노드에 지시하는데 - 여기서 노드는 이미지를 가져올 저장소 주소가 필요하다. 기본적으로는 Docker Hub 같은 공개 레지스트리를 보거나 따로 지정된 Harbor 같은 사설 레지스트리를 본다. - 내가 만든 이미지를 Harbor 레지스트리에 push하지 않으면 이미지가 노트북 로컬 Docker 안에만 있으니 쿠버네티스 노드들(클러스터)은 그 이미지를 찾을 수 없고 Pod 상태가 ImagePullBackOff로 빠진다. - 결론 - build만 하면 내 노트북 안에만 있고 - push까지 해야 Harbor 레지스트리에 올라가서 쿠버네티스 노드들이 거기서 이미지를 pull해서 컨테이너를 실행할 수 있다. FastAPI 컨테이너 만들기 Harbor에 정상 등록됐는지 확인 https://amdp-registry.skala-ai.com/ 접속해서 떠있는지보기. 쿠버네티스 배포 - 배포? - 내가 만든 이미지를 클러스터에서 실행 가능한 애플리케이션으로 올리기. - 배포정보 - deploy.yaml - 어떤이미지를 쓸건지 (image: amdp-registry.../sk019/myapp:latest) - 몇 개의 Pod를 유지할 건지 (replicas: 1) - 어떤 포트를 열 건지 (containerPort: 8080) - 라벨(sk019-myfirst-api-server) - 배포명령실행 1. kubectl은 API Server에 deploy.yaml 내용을 전달 2. API Server는 etcd(쿠버네티스 데이터 저장소)에 “이런 Deployment를 유지하라”라는 선언을 저장 3. 스케줄러가 클러스터 노드 중 하나를 선택, 해당 노드의 kubelet이 “이 Pod는 이 이미지를 써야 해”라고 파악한 뒤, 컨테이너 런타임(docker/containerd)이 Harbor 레지스트리에서 이미지를 pull해온다. 4. 이미지가 잘 내려받아지면 컨테이너가 시작되고, Pod 상태가 Running으로 바뀐다. - 네트워크 구성, pofr-forward 실행 - Pod는 내부 IP가 매번 바뀌기 때문에 Pod가 뜨더라도 외부에서 바로 접속할 수는 없고 그래서 service.yaml로 Service 리소스를 만들고 label을 기준으로 Pod와 연결. - 포트 포워딩 - Service가 생겼다면, 로컬에서 테스트할 수 있도록 포트를 터널링한다. http://localhost:8080/api/users로 접속하면, 사실은 클러스터 안 Pod까지 트래픽이 전달된다. - http://localhost:8080 접속해보면 제대로 뜬다. 포트포워딩 의문점1 - 포트 포워딩이 그래서 하는것은? - 쿠버네티스 Pod는 클러스터 내부 네트워크(IP 대역)에서만 접근 가능하고 내 노트북 브라우저에서 직접 Pod IP를 찍어도 접근이 안됨. 즉 내 PC -> 쿠버네티스 클러스터로 가는 길은 막혀있다. - port-forward는 임시 터널로써 `kubectl port-forward` 명령을 쓰면 내 PC의 포트와 클러스터 안 리소스(Pod 또는 Service)의 포트를 직접연결한다. - `kubectl port-forward svc/sk019-service 8080:8080`하면 내 PC 로컬 8080 포트로 들어오는 요청을 클러스터 안 sk019-service의 8080 포트로 바로 보내는 터널을 만든다. 포트포워딩 의문점2 - http://localhost:8080/api/users로 접근하면 클러스터 안 Pod까지 간다? - 포트포워딩이 걸린 상태에서 `http://localhost:8080/api/users`로 접속하면 - 브라우저는 “로컬 8080”으로 요청을 보냄 - kubectl이 이 요청을 가져가서 클러스터 안 Service -> Pod으로 전달 - Pod 안의 Spring Boot 애플리케이션이 /api/users 요청을 처리하고 응답을 돌려줌 - 응답이 다시 포트포워딩을 통해 내 PC의 브라우저로옴 - 결과적으로는 - 내 PC localhost:8080에 접속한 것처럼 보이지만, 실제 "처리"는 클러스터 안 Pod가 한다. - 결과적으로는에서 말하는 "처리"란? - 브라우저 주소창에 http://localhost:8080/api/users를 입력 -> 브라우저가 HTTP 요청 패킷을 생성해서 내 PC의 8080 포트로 보냄 -> kubectl port-forward가 이 요청을 받아서 클러스터 안으로 전달(Kubernetes Service안으로 던짐) - 클러스터 안에서? - Service가 label로 연결된 Pod를 찾아서 트래픽을 넘김(label로 연결된 Pod = Spring Boot 컨테이너가 들어 있는 Pod) -> Pod 안에는 내가 만든 Spring Boot 애플리케이션이 실행 중. - Pod 안에서? - 컨테이너 안에서 Java 프로세스가 떠 있고, 8080 포트를 열어놓고 있다. Spring Boot는 /api/users라는 URL 요청을 Controller 클래스에 매핑해 둔다. 예를 들어 UserController라는 클래스에 @GetMapping("/api/users")가 있다면, 요청이 오면 그 메서드가 실행되고 JSON 응답(예: [{id:1, name:"Alice"}, {id:2, name:"Bob"}])을 생성해서 HTTP 응답으로 내보낸다. Pod가 만든 응답은 Service -> port-forward 터널 -> 내 PC의 localhost:8080을 거쳐 브라우저로 돌아온다. - 결론 - 브라우저 입장에서는 그냥 로컬에서 프로그램이 실행된 것처럼 보이지만 실제로는 클러스터 안 Pod가 로직을 수행하고 응답을 돌려준것. - 요청 = /api/users - 처리 = Spring Boot 애플리케이션이 Controller/Service/Repository를 통해 데이터 조회/가공 - 응답 = JSON 결과를 브라우저로 반환
#SKALA #Kubernetes
2025-09-02 ⋯ DBMS 및 SQL 활용 #5 Vector DB 스키마 설계
1. 개념 KNN vs ANN KNN과 ANN의 공통 목적 - 질문을 하고 그 질문과 비슷한 질문이나 답변을 데이터베이스에서 찾기 구현 차이 - 모든 데이터를 하나하나 다 비교해서 가장 가까운 것을 찾는다(KNN) - 데이터 전체를 다 비교하지 않고 인덱스를 이용해서 후보군을좁혀서 그 안에서만 비교(ANN) - 친구가 수십만 명 있으면 모든 친구에게 질문을 던져서 과거 답변을 확인하는 대신 비슷한 취향을 가진 대표 그룹 몇 개를 빠르게 찾고 그 안에서만 가장 가까운 답을 고르는 방식. 그러면 인덱스는 비슷한취향그룹 찾는데만 쓰고 그룹 안에서는 knn인가? - 맞음 - 스텝(툴): 후보군 좁히기(ann) -> 후보군 내부 검색(knn 등) DB의 목적 일반적인 db는? - 숫자, 문자열 같은 정형화된 값을 행과 열로 저장하고 필터링과 조인을 수행해서 원하는 정보를 뽑아낸다. 원하는 정보 뽑아내기? - 조건에 맞는 행만 걸러내기 - ex) 나이가 20세 이상인 학생만 찾기 (`SELECT * FROM 학생 WHERE 나이 >= 20;`) - 서로 다른 테이블을 연결해서 더 풍부한 정보 만들기 - ex2) 학생 & 수강 테이블 조인을 통해 "홍길동 학생이 수강하는 과목 목록" 같은 테이블 만들기 (`SELECT 학생.이름, 수강.과목명` `FROM 학생` `JOIN 수강 ON 학생.학번 = 수강.학번;`) 일반적인 db와 벡터 db의 차이 일반적인 DB는 정확한 값을 기준으로한다. - 예를들면 학생 이름이 "홍길동"인 데이터를 찾고 싶다면 `WHERE 이름 = '홍길동'` 같은 조건을 써서 완전히 일치하는 값을 찾는다. - "값이 같은지 여부"라는 불(boolean) 논리에 기반해 검색과 조인을 수행. 벡터 db는 정확한값이 아니라 "얼마나 비슷한가"라는 정도를 계산한다. - "얼마나 비슷한가" 기준? - 벡터 간 distance 또는 similarity - 텍스트, 이미지, 오디오 같은 데이터는 숫자 하나로 일치 여부를 판별할 수 없기 때문에 임베딩을 통해 벡터 공간에 투영한 뒤 그 벡터가 서로 얼마나 가까운지를 측정한다. - 예를들면 "강아지"라는 단어를 검색했을 때 정확히 "강아지"라는 텍스트만 주는 게 아니라 "개", "강쥐", "멍멍이" 같은 비슷한 개념을 함께 찾아줄수있다. 메타데이터 벡터 검색만 하면 - 비슷한 벡터를 찾아줄 뿐 의미는 알려주지못함. - 비슷한 벡터를 찾을때 모두 가져올 뿐 날짜 등 필터링은 못함. 메타데이터가 있으면 - 사용자가 입력한 텍스트와 비슷한 문서를 벡터 검색으로 찾고 그 문서의 제목·저자·링크 같은 메타데이터를 함께 보여줄수있다 - 벡터 유사도로 후보를 먼저 고른 뒤 메타데이터로 Query Filtering을 하면 사용자가 원하는 결과를 정확히 얻을 수 있다. 동적 업데이트 -> 데이터가 계속 들어오거나 수정될때를 고려 Incremental Indexing(점진적 인덱싱) - HNSW - 그래프기반 인덱스 구조 - 데이터가 노드, 비슷하면 엣지가있음 - 새로운벡터가 들어오면 그벡터가 노드가 됨 즉 새로운 데이터(벡터)가 들어와도 기존 그래프(인덱스)가 유지돼서 데이터가 계속들어와도 검색 성능이 떨어지지 않으면서 반영된다. Lazy Update(지연 업데이트) - 새로운벡터가 들어와도 즉시 반영하지않고 일정 시간이 지나면 한꺼번에 인덱스에 반영 - 자원을효율적으로 쓸수있다. Delete & Rebuild(삭제후 재구성) - 시간이 지나면 쓸모없는 데이터가 쌓이기때문에 일정 시간이 지나면 불필요한 벡터는 지우고 인덱스를 재정리해서 최적화해야 검색 속도가 유지되고 공간 낭비를 막을수있다. 결론 - 평소에는 Incremental Indexing과 Lazy Update로 작은 변화들을 처리하다가 주기적으로 Delete & Rebuild를 해서 전체 구조를 최적화한다. Chunking(청킹) 모델은 한 번에 처리할 수 있는 길이에 제한이 있고 긴 텍스트를 그대로 벡터화하면 중요한 부분이 묻힌다. - 그래서 청킹해서 데이터를 자른다 고정 크기 방식 (Fixed Size Chunking) - 1,000자짜리 문서를 200자로 잘라 5개로 만들기. - 간단하고 구현이 빠른데, 문장이 잘리거나 의미가 끊길 수 있다. 의미 기반 방식 (Semantic Chunking) - 단순히 길이가 아니라 내용의 의미 단위 즉 문단, 주제, 혹은 문맥이 바뀌는 지점에서 나눈다. 덩어리 하나가 온전한 의미를 담고 있어 검색이나 답변 생성에서 품질이 좋아진다. 중첩 방식 (Overlapping Chunking) - 데이터를 자를 때 앞 조각과 뒤 조각이 일부 겹치도록 하는 방식, 예를 들어 200자 단위로 자르되 다음 청크는 앞에서 50자를 다시 포함시키는데 이렇게 하면 문맥이 잘려 나가는 문제를 줄일 수 있다. 요약 기반 방식 (Summarization Chunking) - 긴 텍스트를 직접 다루기 힘들 때, 아예 요약을 해서 작은 덩어리로 줄여서 검색할 때는 요약된 덩어리만쓰는건데 검색 속도가 빨라지고 컨텍스트 길이를 절약할 수 있지만 요약 과정에서 중요한 세부 정보가 사라질 수 있다. 계층적 방식 (Hierarchical Chunking) - 텍스트를 먼저 큰 단위(챕터)로 나누고, 그 안에서 절, 문단 단위로 세분화한다. - "문단 단위로 세분화" - 1장 2장으로 나누고 1장을 1.1, 1.2절로 나누고 1.1절을 첫번재문단 두번째문단 일케 나눈다. - 문단만 최종 결과물인게 아니고 1장 같은 큰 단위도 쓰고 1.1절 같은 중간 단위도쓰고 문단 같은 작은 단위도 쓰므로 따로따로 결과물로 저장한다.
#SKALA #DBMS
2025-08-28 ⋯ DBMS 및 SQL 활용 #4 pgvector 기반 유사도 검색 + FastAPI 연동
1. 실습 시나리오 2. 코드 SQL 유사도 검색 vector_search_api.py client.py 터미널 실행 3. 코드설명 SQL 유사도 검색 - embedding_vector vector_cosine_ops - embedding_vector 컬럼을 대상으로 인덱스를 생성 - 코사인 거리(cosine distance)를 기준으로 유사도 검색을 최적화 - WITH (lists = 100) - ivfflat는 전체 벡터 공간을 리스트로 나눠서 가장 가까울가능성이 높은 그룹에서 탐색하는 기법을 쓰는데 → 100개의 리스트로 나눠 탐색한다. - embedding_vector vector_l2_ops - embedding_vector 컬럼을 대상으로 인덱스를 생성 - L2 거리(유클리드 거리)를 기준으로 유사도 검색을 최적화 - random_vector() 목적 - 성능 실험용으로 길이 384짜리 난수 벡터 생성 - array_agg(random())::vector(384) - array_agg(random()): 0 이상 1 미만 난수 384번 생성해서 384차원 배열 생성 - ::vector(384): 벡터로 변환 - 블록 목적 - LIMIT 5 vs 50, 코사인 vs L2 케이스별 실행 속도 비교 - DO $$ ... $$ - 익명 PL/pgSQL 블록 (DB에 저장되지 않는 블록) - DECLARE - 블록 안에서 사용할 Timestamp 변수 t1, t2를 선언 - BEGIN … END; - 실제 실행할 로직을 작성 - t1 := clock_timestamp(), t2 := clock_timestamp() - t1에 시작 시간, t2에 끝 시간 저장 - PERFORM id, title - PERFORM: 쿼리 실행 - LIMIT 5, LIMIT 50 - 가장 유사한 문서 5개만 찾을 때와 50개 찾을 때. - FROM design_doc ORDER BY embedding_vector <=> random_vector() - embedding_vector와 랜덤으로 만든 벡터(random_vector())의 코사인 거리를 계산해서 정렬 - FROM design_doc ORDER BY embedding_vector <-> random_vector() - embedding_vector와 랜덤으로 만든 벡터(random_vector())의 L2 거리를 계산해서 정렬 vector_search_api.py - search_vector() 목적 - 클라이언트가 벡터를 보내면 DB에서 가장 비슷한 문서들을 찾아서 반환 - @app.post("/search") - HTTP POST 요청이 /search 경로로 들어오면 search_vector 함수를 실행. - get_db_conn() - PostgreSQL 연결 생성 (psycopg2) - conn.cursor() - SQL 실행을 위한 커서(cursor) 객체 생성 - query - 입력 벡터와 가장 코사인 거리가 가까운 문서 N개를 찾는 쿼리 - embedding_vector <=> %s::vector - Python에서 넘긴 벡터 문자열을 vector 타입으로 가져오는데 정렬 기준은 코사인 거리 - "[" + ",".join(map(str, data.vector)) + "]” - 클라이언트가 보낸 vector(리스트)를 문자열로 바꿔서 PostgreSQL의 vector 타입으로 해석되게. - cur.execute(query, (vector_str, data.limit)) → rows = cur.fetchall() - 쿼리 실행 & 결과(rows) 가져옴 - return … - DB에서 가져온 튜플들을 JSON 응답 형식으로 반환 - except Exception as e: raise HTTPException(status_code=500, detail=f"DB error: {str(e)}") - DB 연결 실패, 쿼리 오류 등이 나면 500 Error 처리. client.py - cur.execute("SELECT id, title, content, embedding_vector FROM design_doc WHERE id = 1;") - 첫 번째 문서를 기준 문서로 사용할예정이므로 design_doc 테이블에서 id=1인 문서 조회 - requests.post("http://127.0.0.1:8000/search") - HTTP POST 요청: 로컬에서 실행 중인 FastAPI 서버 주소 http://127.0.0.1:8000/search로 - json={"vector": vec, "limit": 1} - 기준 문서에서 뽑아온 벡터(vec)와 가장 가까운 문서 1개 요청 - response.json()["results"][0] - 결과 리스트의 첫 번째 요소(가장 유사한 문서) 가져오기 4. 실행 결과 및 해석 성능 비교 (LIMIT 5 vs LIMIT 50) & (cosine vs L2) - LIMIT 5 vs LIMIT 50 - LIMIT 5: 9.582 ms - LIMIT 50: 4.426 ms - LIMIT 50이 LIMIT 5보다 약 5ms 더 빠르게 수행됨. - cosine vs L2 - cosine: 6.079 ms - L2: 4.114 ms - L2 연산이 cosine 연산보다 약 2ms 더 빠르게 수행됨. - 결과 해석 - LIMIT 값이 크다고 무조건 느려지지 않았는데, 실행 시간은 LIMIT 값에 비례하지 않을 수 있고 이는 ivfflat 인덱스를 사용할 때는 “몇 개를 더 읽어오느냐”보다 “인덱스에서 후보군을 어떻게 선택하느냐”가 더 중요하기 때문일수 있다 - ivfflat은 “전체 데이터를 다 보지 않고, 후보군(클러스터)만 먼저 고른 뒤, 그 안에서 정렬해서 결과를 뽑는 방식”인데 - LIMIT 값이 작든 크든 먼저 후보군을 고르고 정렬하는 과정은 거의 똑같은데 실제로 시간이 더 걸리는 건 “후보군 선택과 정렬”이지 LIMIT 5에서 5개를, LIMIT 50에서 50개를 뽑는 그 ‘추출 단계’ 자체는 별로 비중이 크지 않기 때문일 수 있다 - 그래서 LIMIT 값이 크다고 무조건 느려지지 않았던것일수있다. - L2(<->)가 코사인(<=>)보다 빠르게 나왔는데 L2 거리는 그냥 좌표 차이 제곱해서 더하는 계산이고 코사인 거리 = 내적 계산 + 벡터 크기(norm) 계산이 필요하기 때문에 연산이 더 복잡하므로 시간이 더 소요되는 것이 정상적인 결과 - 실제 서비스에서 속도만 중요하다면 L2를 쓰고 의미적 유사도(문장의 방향성)가 더 중요하다면 코사인을 쓰는 게 맞을수있다 - 벡터 길이가 384차원이고 쿼리도 정렬 기반인데 모두 10ms 이내라면 인덱스가 잘 적용되고 있는 것으로 보이고 - 인덱스가 없었다면 후보군 없이 전체 데이터를 일일이 다 비교해야 해서 시간이 훨씬 소요되는데 ivfflat이 후보군을 뽑아서 연산 범위를 줄여줬기 때문에 시간이 많이 감소하였다. FastAPI 서버 실행 및 클라이언트 실행 - 실행 내용 - DB의 id=1번 문서를 쿼리로 사용해서 가장 유사한 문서 1개를 반환했고 id=1번 문서가 반환 - 결과 해석 - 쿼리로 준 문서 벡터 id=1와 가장 가까운 것은 id=1이므로 그대로 반환 5. 개념 - ivfflat? - 일반 텍스트 검색이나 숫자 검색은 B-Tree 인덱스를 많이 쓰지만 - 벡터 검색은 고차원 벡터 간 거리 계산이 필요하기 때문에 가장 가까울 가능성이 높은 그룹에서만 검색하는 근사 최근접 탐색(ANN, Approximate Nearest Neighbor) 기반으로 유사한 데이터를 찾아서 탐색속도가 빠른 ivfflat를 쓴다. - 인덱스 생성하는 이유? - 문서 의미가 얼마나 방향이 비슷한지를 빠르게 찾기위해서. - 인덱스가 문서 의미가 얼마나 방향이 비슷한지를 빠르게 찾는데 필요한 이유? - 인덱스 없는 경우 - design_doc 테이블의 모든 행에 대해 embedding_vector와 query_vector의 코사인 거리를 계산하므로 10만 건 데이터가 있으면 10만 번의 384차원 내적 연산을 수행. - 인덱스 있는 경우 - USING ivfflat (embedding_vector vector_cosine_ops) 하면 벡터 공간을 리스트 여러개로 미리나눠두고 가장 가까울 가능성이 높은 리스트 몇 개만 선택해서 선택된 리스트 안에서만 거리를 계산한다. 비슷한 후보군 안에서만 비교하기 때문에 속도가 훨씬 빨라진다. - 익명 PL/pgSQL 블록 사용 장점? (함수나 프로시저로 저장하지 않고 일회성 코드 블록으로 실행하는 이유?) - 간단히 성능 테스트, 데이터 초기화, 실험을 할거라서 굳이 DB 객체(함수·프로시저)를 생성하고 저장할필요가 없어서 실행 후 흔적이 안남게함. - 일반 SQL로는 안 되는 로직(변수 선언, IF 조건문, LOOP 반복문)을 실행할수있어서.
#SKALA #DBMS
2025-08-28 ⋯ DBMS 및 SQL 활용 #3 집계함수, 고급 객체기능, 고급 인덱스
1. GROUPBY GROUP BY - 테이블 안에 있는 데이터를 특정 기준으로 묶어서 요약. - 테이블 embedding_store에서 - id, user_id, cluster_id, similarity, tag 5개 컬럼이 있는데 - 있는 그대로보면 큰 그림을 보기 힘들다 즉 해석이 어렵다. - GROUP BY를 쓰면 요약 정보를 만들수있는데 - user_id로 묶으면 “사용자 A는 총 10건, 사용자 B는 총 5건” 같은 식으로 정리 / cluster_id로 묶으면 “클러스터 1은 평균 유사도가 0.8, 클러스터 2는 0.5” / tag로 묶으면 “계약 태그는 100건, 고객상담 태그는 30건” 같은 결과가 나오고 이렇게 하면 데이터의 전체 분포와 패턴을 이해할 수 있다. AI 연계? - 벡터 데이터에서 클러스터링을 하고 나면 각 클러스터의 특징을 봐야되는데 - SQL로 GROUP BY cluster_id를 해서 평균 유사도, 최소 유사도 등을 구해서 평균 유사도가 지나치게 낮은 클러스터가 발견되면 “이 클러스터는 불분명하게 묶였네” 이런식으로 클러스터를 판단할수있다 - SQL로 GROUP BY tag 해서 클러스터내 사용자별 태그 분포를 보면 어떤 사용자가 어떤 패턴을 많이 보이는지를 확인할수있다. - 이런식으로 단순 SQL 집계가 단순 통계가 아니라 이상치 탐색, 품질 저하 감지, 태그 자동 분류 같은 AI 전처리 과정에 활용 가능. Vector DB 분석에서 GROUP BY 활용 - 벡터 데이터는? - 문장, 이미지 같은 걸 임베딩해서 저장해둔 값 - 클러스터링을 하고 나면 각 클러스터가 잘 묶였는지를 확인해야 하고 이때 GROUP BY cluster_id로 묶어서 평균 유사도를 보면 클러스터를 판단할수있다 - 여기서 평균 유사도가 0.9 이상이면 잘 뭉쳐진 클러스터일 가능성이 크고 0.5 이하라면 내부 데이터가 제각각이라 불분명하게 묶인 클러스터라고 판단가능 - 이렇게 SQL 집계로 클러스터 품질을 확인할수있다. AI 결과 검증에서 GROUP BY 활용 - AI 모델이 분류 작업을 했을때 - 실제 라벨(true_label)과 예측 결과(pred_label)가 테이블에 있고 카테고리별 정확도를 구할 수 있다 - 이렇게 하면 “카테고리 A의 정확도는 0.95, 카테고리 B는 0.62” 같은 결과가 나오니까 어떤 클래스에서 모델이 잘 못 맞추는지 바로 확인할 수 있고 이는 모델 개선 포인트로 이어진다. 추천 시스템에서 GROUP BY 활용 - 추천 시스템에서는 사용자가 어떤 아이템을 자주 고르는지, 또는 어떤 유형의 아이템을 선호하는지를 분석해야 하는데 - 사용자별 선택 기록을 GROUP BY user_id나 GROUP BY item_category로 묶으면 개인의 선호를 확인 가능하다 - 이렇게 하면 “사용자 A는 주로 액션 영화를 많이 선택, 사용자 B는 로맨스 위주” 같은 패턴이 보이고 이를 활용해서 토대로 개인화 추천을 강화할 수 있다. 분류 성능 비교에서 GROUP BY 활용 - 분류 모델이 여러 개 있다면 카테고리별로 각 모델의 성능을 나란히 비교할수있다. - 이렇게 하면 “모델 A는 카테고리 X에서는 정확도가 높지만, 카테고리 Y에서는 낮다” 같은 판단(비교) 가능. 2. ROLLUP & CUBE sales_summary 테이블 - 지역(region), 제품(product), 매출액(amount) - East 지역의 A 제품 매출 100, B 제품 매출 150 / West 지역의 A 제품 200, B 제품 50 - 일반적인 GROUP BY region, product를 쓰면? - SUM()으로 합계를 계산했고 그대로 네 줄이 다시 나오면서 매출액이 합계로 정리된다 - 그런데 이렇게 하면 지역별 합계나 전체 합계를 따로 보려면 다시 쿼리를 작성해야함. - GROUP BY ROLLUP(region, product)를 쓰면? - 네 줄의 상세 데이터에 더해서 지역별 소계와 전체 합계까지 자동으로 붙는다. - East 소계: East 지역은 A 100, B 150을 합쳐 250 - West 소계: West는 A 200, B 50을 합쳐 250 - 전체 합계: 500 - 소계를 표시할 때는 product 칸이 NULL로 나타나고 전체 합계는 region과 product가 모두 NULL로 표시. - GROUP BY CUBE(region, product)를 쓰면? - 지역별 합계와 전체 합계뿐 아니라 제품별 합계도 같이 나온다. - East-A, East-B, West-A, West-B 같은 상세 데이터가 나오고 (기본 GROUP BY) - East 전체, West 전체, 그냥 전체 데이터가 나오고 (GROUP BY ROLLUP) - 제품 A 전체, 제품 B 전체 데이터도 나온다. ROLLUP과 CUBE의 차이? - ROLLUP은 계층적으로 요약 - ROLLUP(region, product)이면 - 첫 번째 컬럼(region)을 기준으로 묶고 -> 그 "안에서" 두 번째 컬럼(product)을 묶고 -> 마지막으로 전체 합계까지 올라감 - East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 전체 500 - 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> 전체 500 함. - CUBE는 가능한 모든 조합 - CUBE(region, product)이면 - East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 제품 A 전체 300 / 제품 B 전체 200 / 전체 500 - 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> A/B로 묶고 -> A안에서 East/West로 묶는건 의미없으니 없고 -> 전체 500 함. 3. UDF & 시퀀스 & 저장 프로시저 & UDT & 트리거 (p.95-101) UDF - SQL 문법만으로는 반복적인 계산이나 특정 규칙 적용이 어려운데 - UDF를 만들어놓으면 데이터베이스 안에 내장된 함수 외에도 필요할 때 불러다 쓸 수 있다. - is_similar 함수 - 두 개의 실수값이 주어진 임계치 이상으로 가까운지를 판별하는함수 - 실질적 활용? - 임베딩 스토어에서 코사인 유사도가 일정 기준 이상인 후보만 필터링하는 기능이니까 - 데이터베이스 안에서 바로 AI 예측 후보 선별에 쓸수있다. 시퀀스 - 자동으로 증가하는 고유 ID를 만들어줌 - 테이블에 데이터를 넣을 때 시퀀스를 만들어 두고 nextval로 꺼내 쓰면 순차적으로 값이 올라가니까 데이터마다 일일이 ID를 붙이지 않아도 된다. - 예시 - CREATE SEQUENCE my_seq START 1; -> 이렇게 만들어 두면 - DEFAULT nextval('my_seq')를 컬럼에 달아주면 - INSERT INTO embedding_store (user_text, embedding) 할 때 자동으로 ID가 올라간다 - 매번 새로운 번호가 붙기 때문에 중복 없는 고유 ID를 쉽게 관리할수있다. - 실질적 활용? - 모델 예측 결과나 벡터 데이터가 쌓일 때 결과를 추적하거나 버전을 구분할때 - 벡터를 하나씩 저장할 때마다 고유 번호를 자동으로 달아주면 나중에 “이 임베딩이 어떤 실험에서 나온 것인지”를 관리하기 쉽다. - 결과 추적? - 어떤 문장을 임베딩해서 384차원짜리 벡터를 만들었고 -> 벡터를 테이블에 저장할건데 -> 임베딩은 숫자 배열이므로 나중에 “이 벡터가 언제, 어떤 실험, 어떤 모델로 만들어진 건지”를 추적하기 어려운데 -> 이때 시퀀스로 생성한 고유 ID를 같이 붙여 주면? - 첫 번째 벡터 저장 → ID = 1000 - 두 번째 벡터 저장 → ID = 1001 - 세 번째 벡터 저장 → ID = 1002 - 이렇게 고유 ID가 붙으면 나중에 분석할 때 “ID=1002인 벡터는 실험 X에서 나온 결과다” 하고 연결하기 쉽다. - 버전 관리? - 같은 문장을 두번 실험에 다르게 임베딩했으면 1차 실험 때는 모델 버전 1로 뽑은 벡터 2차 실험 때는 모델 버전 2로 뽑은 벡터가 있을 수 있고 -> 이럴 때 고유 ID를 붙여 두면 “실험 1번에서 나온 ID 1010 벡터와, 실험 2번에서 나온 ID 2020 벡터를 비교하자” 이렇게 버전 관리 할수있다. 저장 프로시저 - 여러 SQL 문장을 묶어 하나의 절차처럼 실행 - 예시 - 예측 결과 테이블 prediction_results가 있고 실제 라벨(true_label)과 모델이 예측한 라벨(pred_label)이 있다. - AI 모델이 예측한 결과를 5개 저장하려고 한다. - 저장 프로시저가 없으면 개발자가 직접 5번 INSERT 문을 날려야하는데 - 저장 프로시저가 있으면 똑같이 5건을 넣어야 하는 상황에서 CALL 한 줄만 쓰면 된다. - 프로시저 내부에 반복문(FOR i IN 1..p_count)이 있어서 알아서 5번 INSERT를 실행해준다 사용자 정의 데이터 타입(UDT) - 보통 테이블 컬럼은 숫자, 문자열 같은 단순 타입인데 내가 원하는 구조를 만들어서 하나의 타입처럼 쓸 수 있다. - 예측 결과를 저장하려고 할때. - 썼을때와 안썼을때의 차이를 보면? - 구체적으로 어디가 다르냐면 - 데이터 넣기 - 안썼을때: (model_name, label, score) -> label과 score를 각각 컬럼에 직접 넣는다. - udt 썼을때: (model_name, result) -> label과 score를 ROW()로 묶어서 result라는 한 컬럼에 넣는다. - 조회 - 안썼을때: SELECT label, score -> 그냥 컬럼 이름(label, score)으로 바로 꺼낸다. - udt 썼을때: SELECT (result).label, (result).score -> result 안에서 필드를 꺼내는 방식으로 꺼낸다. - 의문점 - 출력 결과가 똑같은데 왜쓰는거지? - 답 - 출력 결과만 비교하면 같지만 확장성에서 차이가있다. - 안 썼을 때는 함수가 여러 개 값을 리턴해야 하면 RETURNS TABLE(label TEXT, score FLOAT) 같은 형태로 정의해야 하는데 썼을 때는 함수가 RETURNS prediction_result_type로 정의되니까 “이 함수는 예측 결과 하나를 리턴한다”라고 직관적으로 쓸 수 있다 즉 데이터 구조를 하나의 타입으로 추상화할 수 있다. - 안 썼을 때는 label, score를 다른 테이블에서도 쓰려면 매번 두 컬럼을 복사해야 하는데 썼을 때는 그냥 result prediction_result_type 하나만 선언하면 되니까 중복 정의를 줄이고 일관성 유지 가능(이건 예시에선 2개여서 메리트 없어보이는데 개수 늘어나면 납득됨) - 복잡한 구조 확장 - 예측 결과가 단순히 label+score로 끝나지 않고 label, score, confidence_interval, metadata 같이 커질 수 있는데 안 썼을 때는 컬럼이 점점 늘어나고 테이블마다 다 복사해야 하지만 썼을 때는 타입만 확장하면 모든 테이블·함수에서 동일하게 활용 가능하다. 트리거 - 데이터가 삽입, 수정, 삭제될 때 자동으로 실행되는 규칙 - 예시 - 새로운 벡터가 들어왔는데 유사도가 0.5보다 낮으면 경고 테이블에 따로 기록하려고 할때? - 궁극적인 차이는 - 메인 테이블 + 경고 테이블: 트리거를 쓰든 안 쓰든 구조는 똑같음 - 데이터 넣을 때 - 트리거 안 쓰면: INSERT (메인 테이블), INSERT (경고 테이블, 조건 만족 시) -> N개의 쿼리를 개발자가 직접 작성 - 트리거 쓰면: INSERT (메인 테이블) -> → 1줄만 작성하면 나머지(조건 체크 + 경고 INSERT)는 DB가 자동 처리. 4. 윈도우 함수 (p.126-129) 집계함수와 윈도우함수 차이 - 비슷하지만 GROUP BY처럼 그룹을 한 줄로 압축하지 않고, 각 행마다 순위, 누적합, 이전 값 같은 걸 계산함 - GROUPBY -> 학생(그룹) 단위로 묶어서 한 줄로 결과를 압축했다. - OVER (PARTITION BY student) -> 학생(그룹)별로 평균을 계산하되 결과는 행마다 달아줬다. 윈도우 함수 - ROW_NUMBER() - 그룹 안에서 순번을 매긴다. - 사용자별로 점수를 내림차순 정렬하고 ROW_NUMBER를 매기면, 그 사용자 안에서 1등, 2등, 3등을 구할 수 있다. - RANK() - 동점이 있을 때 같은 순위를 부여하고 건너뛰기가 발생한다. - 1등이 두 명이면 다음 순위는 3등. - DENSE_RANK() - 같은 순위가 있더라도 건너뛰지 않고 다음을 2등으로 붙인다. - NTILE(n) - 데이터를 n개 구간으로 자른다. - 100명을 NTILE(5)로 나누면 성적을 기준으로 20명씩 다섯 구간으로 나눌 수 있다. - LAG() & LEAD() - 현재 행 기준으로 앞 행이나 뒤 행 값을 참고할 수 있어서 시간 순서대로 점수를 나열해 두면 바로 직전 점수와 비교하거나 다음 점수를 미리 볼 수 있다 - SUM() OVER, AVG() OVER - 누적합이나 누적평균 구한다. AI 연계 - 예측 결과를 저장한 prediction_logs 테이블 - 활용 - 여러 모델 버전이 같은 사용자에 대해 점수를 매겼을 때 그중 가장 높은 점수를 고르기. - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY pred_score DESC) -> rownum = 1인 행만 선택 - 모델 간 성능 비교 - RANK() OVER (PARTITION BY user_id ORDER BY pred_score DESC) - 예측 점수 상위 20% 사용자 그룹을 뽑기 - NTILE(5) OVER (ORDER BY pred_score DESC) -> bucket = 1인 행만 선택 - 이전 점수와 비교해 사용자의 점수가 올랐는지 떨어졌는지 확인 - LAG(pred_score) OVER (PARTITION BY user_id ORDER BY created_at) -> pred_score - prev_score 차이 계산 - 모델 정확도의 누적 변화 확인 - SUM(pred_score) OVER (...), AVG(pred_score) OVER (...) 5. 고급 인덱스 (p.144-148) 고급 인덱스? - 일반적인 데이터베이스 인덱스는 B-Tree 인덱스. - AI에서 다루는 데이터는 단순 숫자 키가 아니라 JSON 문서, 벡터, 시계열 로그처럼 복잡하거나 대용량 특성이 있어서 다른 종류의 인덱스들이 필요하다. GIN 인덱스 - Inverted Index: 거꾸로 색인. - 보통 행 -> 마다 단어가 있는데 - "찾고 싶은 단어 -> 그 단어가 들어 있는 행"으로 인덱스를 만든다. - "category"="esg"인 행을 찾고 싶으면 테이블을 처음부터 끝까지 보지 않고 인덱스를 통해 곧바로 1, 3행을 볼수있다. GiST 인덱스 - AI에서 쓰는 벡터 데이터에서의 인덱싱은 - 사전처럼 정확한 값을 빠르게 찾기보다는 이 벡터와 가장 비슷한 벡터를 찾는, 정확히 같은 값이 아니라 가까운 값을 찾는 경우가 많다. - GiST 인덱스는 “거리 기반” 검색을 빠르게 해 주는 구조여서 가까운 것을 찾는 인덱싱에 적합하다. BRIN 인덱스 - 범위별 최소·최대 값만 기록해 두고, 그 안에 데이터가 있을 거라고 좁혀 가는 방식 - 일기장이 날짜 순으로 - 1월 1일~1월 10일 -> 1권 - 1월 11일~1월 20일 -> 2권 - 1월 21일~1월 31일 -> 3권 - 이렇게 적혀있으면 1월 15일 일기를 찾으려고하면 2권만 열어 보면 된다. - 빠른 이유는 범위만 보고 필요한 블록만 열어보면 되기 때문. - 잘 맞는 경우는 로그, 시계열 데이터 - 잘 안 맞는 경우는 무작위 데이터. 왜냐면 “최소~최대”로 구간을 좁힐 수 없기 때문에 범위가 의미가 없다.
#SKALA #DBMS
2025-08-27 ⋯ DBMS 및 SQL 활용 #2 트랜젝션 격리수준, pgaudit, AI 시스템 운영
1. 트랜젝션 격리수준 트랜젝션 - 데이터베이스에서 하나의 작업 단위. - 여러 개의 쿼리나 연산이 묶여 하나로 실행되는데 그 결과는 전부 성공하거나 아니면 전부 실패해서 원래 상태로 되돌아가야 한다. - 그렇지 않으면 데이터가 꼬인다. 문제는? - 여러 사람이 동시에 같은 데이터베이스를 건드린다. - 그래서 데이터가 뒤섞이지 않도록 격리 수준이라는 규칙을 둬야한다. 데이터가 뒤섞인다? - 은행 계좌에서 A 트랜잭션이 “잔액 100만 원에서 10만 원 빼기” 작업을 하고 있고 동시에 B 트랜잭션이 “잔액 100만 원에서 20만 원 빼기” 작업을 한다고 하면 - 각각 따로 실행하면 당연히 최종 잔액은 70만 원이 되어야 한다. - 그런데 둘이 겹쳐서 실행되면 이런 일이 생길 수 있다. 1. A가 잔액을 읽음 → 100만 원 2. B도 잔액을 읽음 → 100만 원 3. A는 100만 원에서 10만 원 빼서 90만 원을 저장 4. B는 자기도 100만 원이라고 알고 있으니까 20만 원 빼서 80만 원을 저장 5. 결과적으로 최종 잔액은 80만 원이 됨 근데 사실 두 번 다 반영되려면 70만 원이 되는 게 맞음. - 결론 - 뒤섞인다 = 여러 트랜잭션이 동시에 실행되면서 서로의 중간 작업 결과가 충돌하거나 덮어씌워져서 최종 데이터가 잘못된 상태로 기록된다. 격리 수준(Isolation Levels) - “내 작업이 다른 사람 작업과 얼마나 떨어져 있나”를 정하는 규칙. - 격리 수준이 낮으면 동시에 빨리 처리할 수 있지만 데이터가 꼬일 위험이 크고 격리 수준이 높으면 꼬임은 막을 수 있지만 속도가 느려진다. Read Uncommitted - 다른 사람이 아직 확정하지 않은 값도 읽을 수 있음 - 작업의 거리가 가까워서 발생할수있는 문제: A가 계좌 잔액을 100만 원에서 50만 원으로 바꾸려다가 아직 완료하지 않은 순간에 B가 그 값을 읽어버리면 B는 50만 원이라는 잘못된 값을 보고 계산을 시작할 수 있음(Dirty Read) Read Committed - 확정된 데이터만 읽을 수 있음 - 같은 데이터를 두번 조회했을때 값이 다를 수 있음. A가 잔액을 조회했을 때는 100만 원이었는데 그 사이 B가 그 값을 200만 원으로 바꾸고 확정해버리면 A가 다시 같은 잔액을 조회했을 때 값이 달라져 있다(Non-Repeatable Read) 의문점 - '같은 데이터를 두번 조회했을때 값이 다를 수 있음'이 왜 문제가 되는가? (당연한거 아닌가 변화가 확정된건데) - 트랜잭션이라는 단위가 가져야 하는 “일관성 보장”이 깨짐. - 트랜잭션은 하나의 논리적 작업 단위인데 즉 그 안에서 여러 SQL 문이 실행될 때 그 문들은 같은 시점의 데이터 상태를 공유한다는 가정이 필요하다. - 예를 들어 트랜잭션 T1이 "잔액을 읽어서 100만 원 이상이면 10% 이자를 주는 UPDATE" 작업을 할때 T1이 먼저 SELECT 잔액을 해서 100만 원이라고 확인 -> 그 사이에 트랜잭션 T2가 잔액을 200만 원으로 바꾸고 커밋 -> 이제 T1이 다시 SELECT 잔액을 해서 계산하려 하면 200만 원이 보임 -> 같은 트랜잭션 안에서 읽은 값이 불일치하므로 T1의 로직은 잘못된 가정 위에서 실행될 수 있다. - 이런 Non-Repeatable Read는 격리 수준을 더 올리면 막을 수 있다. Repeatable Read - 같은 데이터를 여러 번 읽어도 값이 변하지 않는다 즉 내가 한 번 확인한 계좌의 값은 트랜잭션이 끝날 때까지 변하지 않는다. - “고객 수가 몇 명인지” 같은 조건을 걸고 데이터를 읽는 트랜젝션을 수행할때 그 사이에 다른 사람이 새로운 고객을 추가할 경우, 나는 같은 조건으로 다시 조회했을 때 처음보다 고객 수가 늘어난 것을 보게 된다 예를 들어 처음엔 고객이 10명이었는데 다시 보니 11명으로 바뀌어 있다. 이미 본 고객들의 정보는 그대로지만, 집합 자체가 달라진다(Phantom Read). 의문점2 - '이미 본 고객들의 정보는 그대로지만, 집합 자체가 달라진다'가 왜 문제가 되는가? (트랜젝션 자체는 잘돌아갔어도 트랜젝션의 근본적인 목적인 '고객 전체 데이터에 대한 결과 내기'가 안돼서 문제인지?) 답2 - 트랜잭션의 목적(예: 고객 전체 데이터를 기준으로 무언가 계산하거나 판단하는 것)이 제대로 달성되지 못한게 문제다. - 트랜잭션의 목적 - 단순히 SQL을 순서대로 실행하는 것이 아니라 “논리적으로 일관된 하나의 시점(state)을 기준으로 작업을 수행한다”는 걸 보장해서 전체 집합에 대한 일관된 결과를 내는 것이 목적. - 예를 들어 트랜잭션의 목적이 “현재 전체 고객 수를 기준으로 통계를 계산하는 것”일때 - 내트랜잭션을 시작해서 SELECT * FROM customers WHERE condition... 으로 전체 집합을 조회했을 때는 10명이었고 -> 같은 트랜잭션 안에서 이 10명에 대해 뭔가 합계·평균·비율 등을 계산하는데 -> 그 사이에 다른 트랜잭션이 조건에 해당하는 새로운 고객을 INSERT하고 COMMIT해버리면 -> 내가 같은 조건으로 다시 SELECT 하면 이제는 11명이 나와서 -> 내 트랜잭션 안의 앞부분과 뒷부분이 “서로 다른 현실”을 보게 됨 - “고객 전체를 대상으로 한 통계”라는 내 작업의 논리적 일관성을 깨뜨린다. - 요약 - 트랜잭션의 목적이 단순히 한 행을 읽거나 수정하는 게 아니라, “조건에 맞는 전체 집합을 기준으로 어떤 결과를 계산하거나 보장하는 것”이라면 - 격리 수준이 낮으면 트랜잭션 안에서 집합 자체가 변해서 논리적으로 앞뒤가 안 맞는 결과를 낼 수 있고, - 그렇기 때문에 SQL 표준은 이런 현상을 “문제”라고 규정하고, 격리 수준을 통해 제어할 수 있도록 만든 것입니다. 의문점3 - Repeatable Read랑 Unrepeatable Read 차이? 답3 - Non-Repeatable Read (문제 현상) - 트랜잭션 안에서 동일한 조건으로 같은 “특정 행”을 두 번 읽었는데 값이 달라진 경우 - 고객 ID=5번을 첫 번째 조회에서는 나이=30살로 읽었는데 다른 트랜잭션이 그 고객의 나이를 40살로 바꾸면 내가 다시 ID=5번을 읽으면 40살로 보인다. - 같은 행의 값이 바뀌어 반복 불가능한 읽기가 되었다. - Repeatable Read (격리 수준) - Non-Repeatable Read라는 현상을 막기위한 '이미 읽은 행의 값은 트랜잭션 종료까지 고정'이라는 방식. - 고객 ID=5번을 첫 번째 조회에서는 나이=30살로 읽었는데 다른 트랜잭션이 그 고객의 나이를 40살로 바꾸고 커밋하더라도 내가 같은 트랜잭션 안에서 다시 ID=5번을 조회했을 때 여전히 30살로 보인다. - 이미 읽은 행의 값은 트랜잭션이 끝날 때까지 변하지 않는다. - Phantom Read (문제 현상) - 트랜잭션 안에서 동일한 조건으로 "같은 집합"을 두 번 읽었을 때 새로운 행이 끼어들어 결과 집합이 달라지는 경우(기존 행의 값은 변하지 않음) - 나이 ≥ 30살 조건으로 고객 집합을 조회했을 때 10명이었다. 다른 트랜잭션이 나이=35살인 고객(ID=11번)을 새로 INSERT하고 커밋하면 내가 같은 조건으로 다시 조회했을 때 11명으로 보인다. - 기존에 읽은 행들의 값은 그대로지만 집합에 새로운 행이 끼어들어 결과 건수가 달라졌다. 의문점4 - '집합에 새로운 행이 끼어들어 결과 건수가 달라짐'이 왜 문제가 되는가? (고객이 추가된건데 당연한 결과 아닌가? 트랜젝션도 문제없는데) 답4 - 트랜잭션이 한 덩어리의 논리적 작업으로서 동일한 기준(같은 시점·같은 집합) 위에서 결론을 내야 하는 경우는 집합 일관성이 요구되는데 그게 깨져서. - 집합 일관성이 요구되는 경우? - case1: “나이 ≥ 30 고객이 10명 이상이면 VIP 프로모션 집행”이라는 로직에서 1. T1이 처음 조회해 10명을 확인해 프로모션을 집행하기로 결정 2. 그 사이 T2가 1명 INSERT 3. T1이 다시 확인하니 11명 4. 정책 근거의 일관성이 깨짐. 로그엔 “10명이라 집행”이라 찍혔는데, 검증 단계에선 “11명 기준으로 집행됐어야 한다”가 되어 회계/감사·추적 시 앞뒤가 맞지 않게 된다프로모션 집행한다고했는데 예산/재고 산정이 “10명분”으로 계산된 뒤 “11명”으로 검증되면 과소/과다 집행 이슈 발생. - case2: “10명 이하일 때만 집행” 로직에서 1. 첫 조회 10명 -> 집행(YES) 2. 그 사이 1명 INSERT로 11명 -> 동일 트랜잭션에서 재조회 시 미집행 -> '집행여부' 결론 뒤집힘 - case3: “10명 이하일 때만 집행” 로직에서 1. 첫 조회 10명 기준으로 10장 발급 2. 재조회 11명 -> 미발급 1명 발생해서 무결성/공정성 깨짐 - 결론 - 집행 여부가 같아도 근거가 변해 논리적 일관성·정합성이 깨지거나, 결론 자체가 뒤집힘 또는 현시점에 적절하지않은 결론이 도출되어서 트랜젝션 성공 여부와 관련없이 트랜젝션 수행 목적이 제대로 이행되지않는게 문제다. ~*의문점4는 다시보니 의문점2랑 똑같은 질문...*~ Seriesable - 모든 트랜잭션이 순차적으로 실행된 것과 같은 결과를 보장 - 동일한 시점의 데이터를 기준으로 처리하므로 Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생하지 않는다 예를 들어 “나이 ≥ 30 고객이 몇 명인지”를 조회했을 때 처음 10명이었다면, 트랜잭션이 끝날 때까지는 다른 트랜잭션이 고객을 추가하더라도 여전히 10명으로 보이며, 새로운 행이 끼어드는 일이 없다. 의문점5 - Repeatable Read도 트랜잭션이 끝날 때까지 동일한 값이 보장된다고 했는데 Serializable이랑 다른점? 답5 - Repeatable Read - 보장하는 것: 이미 읽은 행(row)의 값은 트랜잭션 종료까지 변하지 않는다. - 보장하지 않는 것: 아직 읽지 않은 “범위(gap)”에 새로운 행이 삽입되는 것은 막지 않는다. - WHERE age >= 30 같은 조건 조회 시, 이미 읽은 고객들의 나이는 그대로지만, 그 조건에 맞는 새로운 고객이 추가되어 “집합”이 달라질 수 있다(Phantom Read) - Serializable - 보장하는 것: 트랜잭션 전체가 직렬(순차) 실행된 것과 동일한 결과 즉 단순히 이미 읽은 행만 고정하는 게 아니라, 조건/범위 전체를 잠가서 새로운 행이 끼어드는 것까지 차단함. - WHERE age >= 30 조건으로 처음 10명이었다면, 내 트랜잭션이 끝날 때까지는 집합이 변하지 않는다. 다른 트랜잭션이 INSERT를 시도하면 내 트랜잭션이 끝날 때까지 대기하거나 충돌로 막힌다. 의문점5 결론 - 집합이 바뀌는건 트랜젝션 수행에 영향을 안준다 << 가 전제되는듯. - 트랜젝션 수행에는 영향이 없고 트랜잭션의 논리적 목표(집합 단위의 일관된 판단/계산)에 문제가 생긴다. - Serializable은 그것마저 차단한다. 격리수준-비유없는 정의 - 동시에 실행되는 여러 트랜잭션 간의 상호작용을 얼마나 차단할지를 정의하는 규칙. - 격리 수준이 낮으면 동시성은 높지만 데이터 일관성이 약해지고 격리 수준이 높으면 데이터 일관성은 강해지지만 동시성이 떨어진다. 2. pgaudit 필요성 - 데이터베이스를 운영할 때 단순히 쿼리가 잘 돌아가는지만 보는 게 아니라, 누가 언제 어떤 SQL을 실행했는지 기록으로 남겨야 함. - 보안 규정이나 법적 규제에서는 “권한 변경이 있었는가, 데이터가 언제 어떻게 수정되었는가, 누가 조회했는가” 같은 사항을 추적할 수 있어야 하고 내부 직원이 부적절하게 데이터를 열람하거나 외부 공격자가 침입했을 때를 대비해 이러한 흔적을 감시할 수 있는 장치가 필요하다 설치 Homebrew PostgreSQL 17 PATH 추가 슈퍼유저 postgres role 생성 후 postgres로 접속 bash psql bash pgaudit 라이브러리 로드 설정 bash sql 주요 설정값 세팅 bash로 하기 sql로 하기 테스트1 테스트2 - DDL/DML 실행후 로그 확인 - CREATE TABLE temp_test(id INT); - 이미 같은 이름의 테이블이 있어서 relation "temp_test" already exists 에러 발생 (정상 동작) - INSERT INTO temp_test VALUES (1); - 두 번 실행됨 - 그래서 id 값이 1인 레코드가 두 개 들어감 - GRANT SELECT ON temp_test TO postgres; - 권한 부여 정상 완료 다시 쿼리 생성해야된대서 다시하기 - FATAL: terminating connection due to unexpected postmaster exit - PostgreSQL 서버가 잠깐 죽었다가(FATAL) 자동으로 재기동 - INSERT INTO temp_test VALUES (1); - 세 번 실행됨 - 그래서 id 값이 1인 레코드가 3개 들어감 - GRANT SELECT ON temp_test TO postgres; - 권한 부여 정상 완료 자꾸 pgAdmin 자체가 실행한 모니터링 쿼리만 뜨는데 ... 머지 ㅠㅠ 3. AI 시스템 운영 AI 파이프라인 - 데이터를 수집하고 정제 -> 벡터화·임베딩을 거쳐 데이터베이스에 저장 -> 그 후 학습과 추론 과정을 통해 모델을 활용 -> 서비스나 API로 결과를 노출 특성? - 각 단계는 담당자와 보안 위험이 다르다. - 수집 단계에서는 민감한 원본 데이터가 노출될 수 있고, 정제 단계에서는 변조가 일어날 수 있다. 임베딩 단계에서는 모델 노출이 위험 요소가 되고, DB 저장은 권한 누수가 문제가 된다. 학습·추론 단계는 반복 호출과 탈취가 이슈이고, 서비스/API 단계에서는 불필요한 노출을 막아야 한다. - 이에따라 ETL 담당자, 데이터 엔지니어, ML 엔지니어, DBA, 서비스 관리자, API 사용자처럼 책임 담당자가 나뉜다. 권한 분리 - 분리 방식? - 수집을 맡은 data_ingestor는 INSERT나 TRUNCATE 권한만, 정제를 맡은 data_cleaner는 SELECT와 UPDATE 권한만, 모델을 다루는 ml_engineer는 SELECT와 실행 권한만 가진다. API 사용자(api_user)는 결과 조회만 허용되고, 최종적으로 admin만 모든 권한과 보안 정책 관리 권한을 갖는다. - PostgreSQL에서 구현 - 벡터 저장 테이블을 만들고 각 역할에 필요한 권한만 부여. - data_ingestor는 INSERT, SELECT, ml_engineer는 SELECT, UPDATE, api_user는 SELECT만 허용하는 식. 데이터 보호 전략 - 민감한 필드는 뷰(View)로 가공해 노출을 제한 - 행 단위 보안(Row-Level Security)을 적용해 “자신이 생성한 데이터만 볼 수 있다” 같은 조건 생성 - 접근 기록은 pgaudit 같은 로깅 확장이나 API Gateway 로그를 통해 남기고 API 키 인증을 통해 모델 접근 제한 API 접근 통제 - FastAPI나 Flask에서 사용자 인증 토큰(OAuth, JWT)을 활용해 접근을 검증 - 추론 요청 시에는 사용자 IP와 쿼리 내용을 저장해 추적 가능성을 확보 - OpenAI나 BERT 같은 대형 모델을 활용할 경우 응답 길이 제한, 시간 제한, 비속어 필터링 - 벡터 검색 결과는 SCORE 기준으로 중요도 있는 일부만 노출되도록 제어해 불필요한 데이터 유출 통제 - GraphRAG 같은 방식은 노드·엣지 단위로 권한을 세분화해 특정 사용자에게 필요한 정보만 노출
#SKALA #DBMS
2025-08-19 ⋯ LLM #2 LLM과 AI 기술요소를 활용하여 비즈니스 서비스 기획안 작성
1. 목적 - 등기부등본/건축물대장 업로드 시 AI가 자동으로 문서를 분석하여 전세사기 위험 요소를 탐지하고 수치화한다. 2. 모델 구성도 데이터 수집및 정규화 - 기술요소: PaddleOCR - 선택 이유: 한국어 인식 정확도와 속도가 좋고, 오픈소스+온프레미스 운영 가능(비용·보안 유리), 표 레이아웃/좌표 추출 지원. - 입력 - 파일: PDF/스캔 이미지(JPG/PNG) - 매개변수: lang="korean", det+rec 사용, dpi(≥300) - 출력 - 텍스트 블록: [{page, bbox, text}] - 정규화 결과: 주소/금액/날짜/권리유형 표준화(JSON) 위험 특약/권리 분석 - 기술요소: RAG - 선택 이유 - 사실 기반 답변: 등기부등본, 계약서, 법률 조항 등 최신 외부 데이터를 활용하여 허위 정보 생성을 방지하고 사실에 기반한 분석 결과를 제공 - 유연성 및 확장성: 새로운 법률 개정, 최신 판례, 특약 유형 변화 등에 맞춰 데이터베이스를 쉽게 업데이트할 수 있어 최신 정보를 반영한 분석이 가능 - 근거 제시: 원본 문서 기반 신뢰할 수 있는 분석 결과 - 입력 - 문서 데이터: PDF/스캔 이미지(JPG/PNG) 형태의 등기부등본, 계약서 사본 - 질의(Query) 벡터: OCR로 추출된 텍스트 블록 중 특약 및 권리 관련 문장 - Vector DB: 특약, 등기부등본 상 권리, 법률 조항, 과거 피해 사례 등 텍스트 데이터를 벡터화하여 저장 - 출력 - 위험 라벨: '선순위 임차인 존재', '가압류', '근저당권 과다' 등 - 근거 스팬: 원본 문서 내 위험 라벨의 근거가 되는 문장 및 위치 - 위험 지수: 특약 및 권리 유형의 위험성을 정량화한 점수 ML 위험 예측 - 기술요소: LightGBM - 선택 이유 - 수치·범주 혼합 데이터에서 빠르고 강력하며 해석·튜닝이 쉽고, 소규모부터 대규모까지 안정적. - 입력 - 재정 지표: 전세가율, 채권최고액/보증금 비율 - 권리 정보: 근저당권 수, 소유권 변경 횟수/최근성 - RAG 결과: 위험 지수 - 출력 - 위험 점수: 0~100점 - 위험 등급: 5단계 LLM 리포트 생성 - 기술요소: GPT-4o - 선택 이유 - 한국어 설명 품질·사실성·형식 제어가 우수, 근거 텍스트/수치 결합 요약에 강함. - 입력(프롬프트 구성) - 메타데이터: 주소, 면적, 보증금, 계약일 - ML 결과: 위험 점수, 위험 등급 - RAG 결과: 위험 라벨 상위 n개와 근거 문장 - 출력 - 자연어 리포트: 근거 하이라이트 포함 - 맞춤형 권고사항: 위험도별 액션 가이드 비고 - 교육과정에서 사용한기술요소로 구성한게 좋다고 하셧는데 RAG로 위험라벨뽑는게 core 로직인데 그걸 구현하는게 매우어려울것같다고하셧다 - RAG 출력인 위험지수를 LightGBM 인풋으로 넣는게 좋다고 해주셧는데 비정형데이터로부터 숫자 정보를 앞에서 뽑아놓은걸 뒤에서 안쓰는게 아까우니까 넣는게낫겟지? 라고 막연하게생각했는데 좋다고 피드백와서조앗다 3. RAG 이해하기 Input - 사용자가 업로드한 계약서 텍스트 - OCR 모듈이 PDF/이미지에서 추출해 JSON 또는 텍스트 형태로 전달한다. - 검색 쿼리 - 계약서 위험 분석을 위해 골라놓은 쿼리 (특약 조항이나 권리 의무 조항. 예를들어 “임대인의 권리 제한 조건은 무엇인가?”, “근저당권 관련 조항은 포함되어 있는가?”) Process (하는일) - 텍스트 벡터화 - 계약서 조항/문장들을 임베딩 모델(e.g., Sentence-BERT, OpenAI Embedding API)로 벡터로 변환. - Vector DB 저장/검색 - 모든 조항을 벡터 DB(예: Pinecone, Weaviate, Milvus, FAISS)에 저장한 뒤, 쿼리 벡터와 유사도 검색을 수행 - 조항 필터링/정규화 - 검색된 조항 중에서 위험 분석에 필요한 "특약/권리" 관련 조항만 필터링 - 리스크 라벨링 - 미리 학습된 ML 모델(또는 룰셋)을 이용해 해당 조항이 위험(High Risk), 주의(Warning), 안전(Safe) 등으로 분류 *미리 학습된 ML 모델? - Raw ML 모델 + “위험 조항 vs 일반 조항” 라벨링 되어있는 계약서 데이터셋 = 사전 학습된 ML 모델. Output - 위험 라벨: ex. High Risk, Moderate Risk, Safe - 근거 문장(조항 원문): 검색된 계약서의 특정 문장/조항 - 메타데이터: 조항 위치, 페이지, 좌표 등 OCR에서 받은 정보 - 예시 출력 4. 더 구체화된 모델 구성도 사실 gpt로부터 얻어낸 초기 모델구성도는 더더 디테일하고 장황했는데 풀어보자면 다음과같았다. 데이터 수집및 정규화 - 기술요소 - OCR: PaddleOCR(korean, layout) 또는 Tesseract(kor+osd) + 문서구역 감지(layout-parser) - 표/구역 파서: pdfplumber, camelot, heuristic 규칙 - Input - 파일: 스캔 이미지(PNG/JPG) 또는 PDF - 메타: dpi, page_range, 언어=ko - Output - 텍스트 블록 목록 + 좌표(bbox), 페이지 인덱스 - 섹션 태깅: 표제부/갑구/을구, 계약서 제목/항/특약 위험 특약/권리 분석 - 구성: 문장/조항 분할 → NER → 관계추출(RE) → 위험 조항 분류 → 규칙 후처리 1. 문장/조항 분할 - 기술요소: KoELECTRA-small(문장경계) 또는 쉬운 대안: pysbd-ko + 규칙 - Input: OCR 정제 텍스트(최대 수천 자) - Output: 문장/조항 토큰열(512 토큰 겹침 윈도우 포함) 2. 개체 인식(NER) - 기술요소: KorFinBERT/KoBERT/Legal-BERT 파인튜닝(토크나 분류) - 라벨: PERSON(임대인/임차인/소유자), ADDR, MONEY(deposit, max_claim), DATE, RIGHT_TYPE(근저당/가압류/가처분…), PRIORITY, ORG/BANK, CONTACT - Input: 조항 단위 토큰열 - Output: 개체 span + 라벨 + 점수 3. 관계 추출(RE) - 사용 모델: Legal-BERT 문장/문맥 쌍 분류(개체쌍→관계), 또는 biaffine 관계추출기 - 스키마: (RIGHT_TYPE–MONEY(max_claim)–DATE(setup)–PRIORITY), (LESSOR↔OWNER match_flag), (CLAUSE↔RISK_KEYWORD) - Input: 개체 주석된 문장 + 후보 개체쌍 - Output: 관계 라벨/점수 4. 위험 조항 분류(다중라벨) - 사용 모델: KorFinBERT/Legal-BERT(Sequence multi-label) + focal loss - 클래스: double_contract, deposit_return_risk, multi_mortgage, frequent_ownership_change, block_move_in, unfair_special_terms 등 - Input: 조항 텍스트(최대 512 토큰) - Output: 라벨별 확률, 최상위 라벨, 근거 토큰 5. 규칙 후처리(하이브리드) - 사용 엔진: 룰 엔진(jsonlogic/自製) - Input: NER/RE/분류 결과, 외부 수치(채권최고액/보증금 비율 등) - Output: 보정된 위험 신호(플래그 및 가중치) 6. NLP 모듈 최종 Output 묶음 2.5 Feature Engineering - Input - 외부 수치: 전세가율, 지역 중앙값 대비 편차, 거래 변동성 등 - 등기부: 근저당 건수, max_claim/보증금, 권리 중첩기간, 소유권 변경횟수·최근성 - NLP: 위험라벨 개수/비율/최대확률, 특약위험지수, 증거문장 수 - Output - 정규화/인코딩된 피처 벡터(X), 타깃(y: 사기/피해사례 라벨 or 위험레벨 라벨) ML 위험 예측 - 사용 모델 - 탐색: AutoGluon/H2O.ai/PyCaret - 본선: LightGBM/XGBoost/RandomForest(+ LogisticRegression baseline) - 확률 보정: Isotonic/Platt - Input - 피처 벡터(X), 학습 시 타깃(y) - 추론 시: 단건/배치 X - Output - 위험 확률(0~1), 등급(저/중/고), SHAP(전역/개별) - 검증/운영 지표 - ROC-AUC, PR-AUC, recall@HIGH, Brier score(캘리브레이션), 시계열 블록 CV LLM 리포트 생성 - 사용 모델 - GPT-4o / Claude 3 Sonnet / LLaMA-3(온프레미스) - Input(프롬프트 구성) - 요약 목표: “전세사기 위험 리포트 생성” - ML: risk_prob, risk_grade, 상위 SHAP 근거(수치) - NLP: 위험 라벨 상위 n개 + 근거 문장 span/원문 - 메타: 주소, 면적, 계약일, 보증금 등 - Output - 자연어 리포트(근거 인용), 권고사항, 하이라이트 포인터 다음 단계들 품질·모니터링 - Input - 추론 로그(입력 해시, 모형버전, risk_prob, 라벨, SHAP), 분포 통계 - Output - 드리프트 경보, 재학습 트리거 이벤트 보안·거버넌스 - Input/Output - PII 토큰화/해시, 암호화 저장, 접근 로그 - 추적성: 모델·피처 버전, 프롬프트·리포트 해시 배포·운영(Ops) - Input - 동기 API(단건) / 비동기 배치(폴더/버킷) - Output - 처리 상태, 리포트 ID, 지연·오류 메트릭 End to End로 입력->출력 예시 생각 1. 먼가어려웠는데 전체흐름을 이해하는게필요할거같아서 1회독을 해보앗다 2. 첨엔 실습 설명 들으면서 먼말인지 1도안와닿았는데 얘기하면서하다보니깐또 하게댓다. 3. 조모임은 부족한내가 나혼자부족하면갠찮은데 외부에 노출대서 영향을줄수도잇다는생각이들어서? 더 부담대고 도망가고싶은거같은데 그럼에도불구하고 multi head의 힘은 확실히있구나라고생각들어서 살면서 조모임력은 필요하다는것을 인정하게되엇다 4. 교수님이 PaddleOCR 언급을 되게오래하시면서 써본사람이잇는건지 어쩌고 하셧는데 그냥 지피티 돌려서 나온건데 생각햇다 .. (지금도 뭔지모름)
#SKALA #LLM
2025-08-18 ⋯ LLM #1 LLM 이해와 Transformer
1. LLM 기본이해 Word Embedding (p.27-28) Word Embedding - 핵심 아이디어는 단어가 어떤 맥락에서 자주 함께 등장하는지를 학습. - “you say goodbye and I say hello”에서 - ‘goodbye’주변에는 ‘you’, ‘say’, ‘and’, ‘I’ 같은 단어가 함께 등장하고 그 관계를 학습하도록 신경망을 훈련시킨다. - 학습이 반복되면 각 단어는 벡터로 표현되고 의미가 비슷한 단어일수록 벡터 공간에서 가깝게 위치한다. - Input이 ‘goodbye’이고 Target이 ‘you’, ‘say’, ‘and’, ‘I’여도 된다. Word Embedding - 신경망 구조 그림 - 왼쪽 단어 목록, 가운데는 은닉층, 오른쪽에 단어 목록 - "eat"이 입력으로 들어가면 은닉층을 거쳐서 출력 쪽에서 "apple", "juice", "rice" 같은 주변 단어들이 활성화되고 이 과정에서 모델은 입력 단어와 주변 단어 사이의 연관성을 학습한다. Reasoning - 만들어진 벡터는 단순한 유사성뿐 아니라 관계까지 담고 있다. - ‘king - man + woman = queen’ - ‘왕에서 남성을 빼고 여성을 더하면 여왕’이라는 단어의 의미 관계가 수학적으로 표현된다. - Reasoning? - 놀이터에서 ‘( )가 나를 바라보고 있다’라는 문장에서 빈칸에 들어갈 수 있는 적절한 단어를 유사한 문장들의 패턴을 통해 추론 가능하다. - 단어 벡터 공간에서의 위치 관계 그림 - king, queen, man, woman 같은 단어들이 점으로 표시되고 상대적인 거리와 벡터 방향이 있으니까 King - Man + Woman = Queen 같은 의미적 연산이 가능하다. 의문점 - Word Embedding이 분포가설의 구현이라고했는데 분포랑 무슨상관이지? - 단어가 어떤 단어들과 자주 같이 나타나느냐의 분포가 그 단어의 의미를 규정한다는 게 분포가설. - Word Embedding은 비슷한 맥락에서 쓰이는 단어들은 비슷한 임베딩 벡터로 표현된다 즉 주변 단어와의 관계가 임베딩 공간에 투영된다. - 단어의 분포적 특성이 수치화되어 벡터 공간에 반영된다 = 분포가설을 계산가능한 형태로 구현한것이다. - 정리 - 어떤 분포를 (즉 평균 분산을) 진짜로 구현한다기보다 '단어의 분포(유사한 단어와 자주 나타나는 정도)적 특성이 있다'라는 이론을, Word Embedding은 주변 단어와의 관계를 비슷한 임베딩 벡터로 표현 즉 수치화함으로써 '구현'했다. RNN, seq2seq, attention (p.30-31) - RNN은 입력된 단어를 임베딩이라는 연속형 벡터로 바꿔서 모델에 넣고 바로 앞까지 처리된 hidden state 벡터와 함께 계산한다. - 예를 들어 what, will, the, fat 같은 단어가 순서대로 들어오면 모델은 마지막 시점에 얻어진 벡터로 다음 단어를 예측한다. 이때 소프트맥스를 사용해 확률 분포를 만들고 가장 높은 값, 예를 들어 0.7이 나온 단어를 선택하는 식이다. 하지만 이 구조는 오래된 정보가 뒤로 갈수록 점점 희미해져서 30~50칸 전의 단어는 사실상 기억하기 어렵다는 장기의존성 문제가 생긴다. - seq2seq 모델에서는 인코더가 전체 입력 시퀀스를 읽고 그 맥락을 하나의 컨텍스트 벡터로 압축, 컨텍스트 벡터를 디코더가 받아서 출력 시퀀스를 생성한다. 영어 문장을 인코더에 넣으면 전체 문장이 하나의 벡터로 변환되고 그 벡터를 토대로 디코더가 프랑스어 문장을 순서대로 만들어낸다. 하지만 문장이 너무 길면 이 하나의 벡터가 과도하게 많은 정보를 담아야 해서 정보 소실이 발생한다. - 어텐션은 입력 문장을 하나로 압축하지 않는데 인코더가 만들어낸 모든 hidden state도 사용한다. - 사용 = 디코더가 단어를 출력할 때마다 인코더의 전체 hidden state 중에서 어떤 부분을 주목할지 점수를 계산하고 그 점수에 따라 필요한 정보를 골라온다. (hidden state = 인코더의 output인 문맥 벡터) - 예를 들어 번역에서 “it”이라는 단어를 생성하려 할 때, 인코더 입력 중에서 “animal”인지 “street”인지 같은 후보들에 대해 각각 점수를 매기고 가장 관련이 높은 단어를 참고하는 방식이다. - 이렇게 하면 입력 전체를 다시 들여다볼 수 있으므로 문장이 길어도 특정 단어와의 연결 관계를 놓치지 않는다. - 또한 입력을 순차적으로만 처리하지 않고 병렬적으로 계산할 수 있다. RNN처럼 컨베이어 벨트 방식으로 단어를 하나하나 넘기는 대신 전체 입력 문장에서 각 단어와의 연관성을 한 번에 계산하기 때문에 연산 효율이 좋아지고 장기의존성 문제도 해결된다. Contextual Embedding (p.32) - Word Embedding은 “단어 하나 = 벡터 하나”라는 고정 표현을 만든다. - 그림에서 “bank”라는 표기가 좌표평면에 점 하나로 찍혀 있고 이건 돈의 bank인지 강둑의 bank이든 한 벡터에 섞여버린다. - 트랜스포머 기반의 Contextual Embedding은 같은 철자라도 문맥이 바뀌면 다른 벡터를 생성한다. self attention에서 입력 문장의 각 토큰이 주변 모든 토큰을 참고해 자기만의 문맥 표현을 만들기때문에 최종 hidden state(=그 토큰의 임베딩)가 글자가 같아도 문맥에 의존해 달라진다. - “He deposited money in the bank”에서 bank의 벡터는 money, deposit, loan 같은 단어에 높은 어텐션 가중치를 주며 금융 의미 쪽으로 이동한다. - “They had a picnic on the river bank”에서는 river, shore, picnic에 주목해 물가 의미 쪽으로 이동한다. - 좌표평면 그림에서 - bank가 문맥에 따라 “돈/대출” 근처에 위치하기도 하고 “강/물가” 근처에 위치하기도 한다. Transfer Learning (p.33) - 딥러닝의 기본은 복잡한 문제를 풀기 전에 여러 중간 단계를 거쳐 추상적인 개념을 점차 쌓아가는 표현 학습(임베딩). - 비슷한 문제에서 사전 학습된 모델이 이미 학습해 둔 개념들(임베딩 벡터)을 가져와 특정 문제를 푸는 방식이 Transfer Learning. 2. 유사도 Cosine Similarity (p.45-48) 개념 - 두 벡터가 서로 얼마나 같은 방향을 가리키는가? - 수학적 개념: 두 벡터의 내적을 각 벡터의 크기로 나누어 정규화한 값 (정규화=크기는 사라지고 각도 즉 코사인만 남는다) 유사도 판단 (수치) - 두 벡터 사이 각도는 0도이면 코사인은 1이 되고 유사도는 최대치인 1로 계산된다. - 두 벡터가 정반대 방향이라면 각도는 180도가 되고 코사인은 -1이 되고 유사도가 최소가 된다. - 두 벡터가 직각이라면 각도가 90도가 되고 코사인 값이 0이 되고 벡터 사이에 방향성의 유사성이 전혀 없다고 해석한다. - cf) 유사도가 최소이다 vs 방향성의 유사성이 전혀 없다. - 최소는 -1이고 0이기만해도 유사성은 전혀 없다. - 실제 계산에서는 보통 모든 성분이 양수인 경우가 많기 때문에 유사도의 최소값은 0으로 취급하는 경우가 많다고함. 유사도 판단 (실제 case) - 단어의 빈도수가 달라져도 방향이 비슷하다면 코사인 유사도가 높다. - 문서 하나가 apple과 banana를 각각 한 번씩 포함하고 또 다른 문서가 apple과 banana를 세 번씩 포함했다면 두 문서의 벡터는 크기는 다르지만 방향은 같다. - 그래서 코사인 유사도는 1이 되어 두 문서가 같은 주제를 다루고 있다고 판단한다. case study - 문서1은 apple과 banana를 포함해 (1,1,0)/ 문서2는 apple, banana, carrot을 포함해 (1,1,1) / 문서3은 apple과 banana가 여러 번 반복되어 (3,3,0) 벡터로 표현. - 유클리드 거리를 기준으로 보면 문서1은 문서2와 더 가깝지만 코사인 유사도를 기준으로 보면 문서1과 문서3이 더 가깝다. - 코사인 유사도가 벡터 크기의 차이를 무시하고 방향만 보기 때문에 텍스트 데이터처럼 길이가 달라도 같은 주제를 다룰 수 있는 상황에 유용하다(데이터가 크기와는 무관하게 같은 맥락이나 주제를 향하고 있는지를 확인). 3. Transformer Self-Attention 인코딩과 디코딩 (p.55) - 인코딩은 비정형적인 입력을 의미 있는 벡터로 바꾸는 과정이다. - 디코딩은 이 벡터를 기반으로 새로운 대상을 생성하는 과정이다. - 예를 들어 문장을 입력하면 인코더가 문장을 수치 벡터로 변환하고 디코더가 이를 이용해 번역된 문장을 만들어낸다. Query, Key, Value (p.56) - 입력으로 들어온 벡터의 크기가 4×10 - 여기에 가중치 행렬을 곱해서 차원을 줄이거나 변형한다. - 예를 들어 10×5 크기의 가중치 행렬을 곱해주면 입력은 4×5 크기로 변환되어 원래 10차원이었던 단어 임베딩 벡터가 5차원 표현으로 바뀌게 된다. - 선형 변환으로 차원을 바꾼 뒤 Query, Key, Value 벡터로 나눈다. - Query는 “내가 누구를 참고할지, 어디에 집중할지” - Key는 “Query가 참고할 수 있는 정보” - Value는 “실제로 전달될 정보” cf) Key가 Query가 참고할수있는 정보라는게 무슨말인지? - Query는 질문이고, Key는 후보 답변. Query는 지금 단어는 누구한테서 힌트를 얻어야 하지? Key는 이 특징이 지금 단어가 찾는 '누구'랑 얼마나 맞을까? - 예시 “I love pizza” - Query: 현재 내가 집중하는 단어 -> “love” - Key: 문장 안의 모든 단어가 가짐 -> I(Key), love(Key), pizza(Key). - Query("love")가 Key들과 내적을 해서 유사도를 보니 Key("pizza")랑 점수가 높으면 “love는 pizza랑 관련이 크다" / Key("I")랑 점수는 낮으면 “love는 I와는 관련이 약하다” 그럼 “love”라는 단어는 “pizza”의 정보를 더 많이 참고해야겠구나 하고 판단. Self-Attention에서 토큰이 자기 자신과 다른 모든 토큰들 사이의 관련성을 계산하는 법 (p.57-60) 1. Query와 Key를 내적해 4×4 크기의 score 행렬을 만든다. (i, j) 위치의 값은 i번째 토큰이 j번째 토큰을 얼마나 주목해야 하는지 score인데 내적 값이 크면 유사성이 높다는 뜻이고 주목해야 할 대상이라는 뜻이다. (이처럼 토큰들 사이의 관련성을 자기 자신 안에서 계산하기 때문에 Self-Attention이라고한다) 2. 내적 값이 너무 커질 경우 특정 항목만 지나치게 강조될 수 있으므로 score를 Key 차원의 제곱근으로 나누어 스케일링하는 과정을 통해 값의 분산이 안정화해서 학습을 안정화한다. 3. Softmax 함수를 적용해 각 행이 합이 1이 되도록 확률 분포로 바꾼다. 이렇게 변환된 값이 Attention Score로, 각 토큰이 다른 토큰을 얼마나 참고할지를 확률 형태로 표현한 것이다. 4. Value 벡터와 이 Attention Score를 곱한다 즉 원래의 정보(Value)를 점수에 비례해 가중합한 새로운 벡터를 만든다. 이 결과는 원래 토큰 벡터를 업데이트한 것과 같다. 즉, 각 토큰이 문맥 속에서 어떤 다른 토큰과 얼마나 연결되어 있는지를 반영해 다시 표현된 새로운 벡터가 생성된다. Multi-Head Attention (p.61-62) Multi-Head 필요성 - Self-Attention을 한 번만 거치면, 특정한 기준(맥락, 의미, 어휘적 유사성 등)에서만 관계를 포착할 수 있다. - 멀티 헤드 어텐션은 이런 Self-Attention을 여러 개 병렬로 실행해서, 서로 다른 관점에서 입력을 바라볼 수 있도록 한다. 예를 들어 어떤 헤드는 단어의 순서적 맥락에 집중할 수 있고, 또 다른 헤드는 의미적 유사성에 주목할 수 있으며, 또 다른 헤드는 특정 어휘 패턴을 따라가며 관계를 본다. 이렇게 여러 헤드가 만들어내는 다양한 관점을 합치면 일종의 앙상블처럼 작동해서 모델은 훨씬 다차원의 표현을 생성하게된다. 학습 과정 - 각 헤드는 Q, K, V를 각각 독립적인 가중치 행렬로 변환한다. 따라서 같은 입력이라도 헤드마다 Q, K, V가 달라지고 그 결과로 나온 Attention Output도 서로 다르다 즉 헤드별로 서로 다른 방식으로 “무엇을 주목할지”를 학습한다. - 각 헤드의 Attention Output은 보통 4×5와 같은 크기의 행렬로 나오는데 여러 헤드의 출력들을 옆으로 이어붙이는 방식으로 결합한다(Concat). - 단순히 붙인 결과는 각 헤드의 특징이 분리된 채로 남아 있어, 모델이 이를 자연스럽게 활용하기 어려우므로 이어붙인 벡터를 다시 한 번 선형 변환(Linear Transformation)해서 하나의 통합된 표현으로 만든다. 이렇게 하면 맥락 정보, 의미 정보, 어휘 정보 등 다양한 관점의 결과가 하나의 일관된 벡터 공간 안에서 재표현되어 이후 레이어들이 이 표현을 자연스럽게 사용할 수 있다. 정규화, 잔차 연결, Feed Forward Layer, Positional Encoding (p.63-65) 정규화(Normalization) - 정규화는 LayerNorm을 통해 이루어진다. 이는 각 토큰 벡터 차원별 평균과 분산을 정규화해서 입력 분포가 일정하게 유지되도록 만든다. 이렇게 하면 학습이 빠르고 안정적이 되며, 그래디언트 소실이나 폭주를 막을 수 있다. 실제로 Multi-Head Attention 같은 연산을 통과하면 값의 크기가 커지거나 불안정해질 수 있는데, LayerNorm을 거치면서 다시 안정된 값으로 조정된다. 잔차 연결(Residual Connection) - 연산 과정에서 원래 입력 정보를 보존하기 위해 사용된다. 예를 들어 어텐션 결과만 계속 쌓아가면 초기 입력의 정보가 소실될 수 있다. 이를 방지하기 위해 원래 입력을 연산 결과에 더해주는 방식으로 정보 흐름을 유지한다. 이렇게 하면 기존 정보 위에 추가적인 정보를 겹쳐 쌓는 구조가 되고, Gradient가 지나갈 통로도 유지되어 학습이 더 잘 이뤄진다. 즉, 단순히 변형된 표현만 쓰는 게 아니라 원래 입력과 변형된 출력을 함께 사용하는 것이다. Feed Forward Layer - 어텐션만으로는 거의 선형 결합만 수행되므로 모델의 표현력이 부족할 수 있다. 딥러닝의 핵심은 비선형성을 주입하는 것인데, 이를 위해 활성함수(Activation Function)를 사용한다. 트랜스포머에서는 보통 ReLU 같은 비선형 함수를 적용해 입력 표현을 더 복잡하고 풍부하게 바꾼다. 이 과정에서 단순한 선형 결합을 넘어 복잡한 패턴을 학습할 수 있게 된다. Positional Encoding - 어텐션 메커니즘은 모든 토큰을 동시에 바라보기 때문에 토큰의 순서를 직접적으로 알 수 없다. 예를 들어 “나는 밥을 먹었다”와 “밥이 나를 먹었다”는 순서가 바뀌면 의미가 완전히 달라지지만, 어텐션만 사용하면 두 문장을 구분하기 어렵다. 이를 해결하기 위해 입력 임베딩에 순서 정보를 더해주는 것이 Positional Encoding이다. 위치 정보를 단순히 정수로 추가하면 값의 범위가 커져서 다루기 힘들기 때문에, 사인(sin)과 코사인(cos) 함수를 이용해 주기적인 패턴으로 위치를 표현한다. 이렇게 하면 어떤 위치든 간결하게 표현할 수 있고, 모델은 순서를 반영한 연산을 할 수 있다. 사인과 코사인은 시간적, 순차적 데이터를 표현할 때 자주 쓰이는 방식인데, 여기서는 토큰의 위치 정보를 수학적 벡터로 만들어 임베딩과 더해주어 순서를 구분할 수 있도록 돕는다. 디코더에서의 Masked Self-Attention (p.66-67) 마스킹(masking) - 트랜스포머의 기본적인 어텐션 메커니즘은 모든 단어가 한꺼번에 보일 때 서로 간의 문맥을 파악하는 구조지만 디코더는 문장을 생성할 때 미래 단어까지 동시에 볼 수 없도록 제한해야 한다. 예를 들어 "I study"까지 입력이 주어졌다면, 그 시점에서 "AI hard"라는 단어들은 아직 주어지지 않은 정보이므로 모델이 참고하면 안 된다. - 이를 위해 마스킹 과정을 거친다. 어텐션 스코어 매트릭스를 계산할 때, 미래 토큰에 해당하는 위치는 마스크 처리해서 무시한다. 따라서 주어진 입력이 "I study"라면, 모델은 오직 "I"와 "study" 사이의 관계만 학습하고 그 이후 단어와는 내적을 하지 않는데 이렇게 하면 디코더는 항상 현재까지의 단어들만을 기반으로 다음 단어를 예측하게 된다. - 마스킹에서 중요한 연산은 Q(쿼리)와 K(키)의 내적을 통해 어텐션 스코어 행렬을 만드는 것이다. 그런데 마스킹이 적용되면 아직 주어지지 않은 단어는 스코어 계산에서 제외된다. 따라서 어텐션 스코어 행렬에는 주어진 토큰까지만 반영된다. 예를 들어 "I"가 입력이면 자기 자신만 참조할 수 있고, "I study"라면 "I"와 "study"만 참조할 수 있다. - 훈련(학습) 과정에서는 모델이 만든 출력과 실제 정답(ground truth)을 비교하면서 학습한다. 예를 들어 "I study"까지 입력했을 때 다음 단어로 "AI"가 올 확률을 모델이 예측하고, 확률 분포를 생성해서 생성한 확률 분포로부터 얻은 예측값을 실제 정답과 비교해서 Loss를 계산한다. 이 손실을 역전파하면서 모델의 weight가 업데이트된다.
#SKALA #LLM
2025-08-13 ⋯ python #2 객체지향 프로그래밍, 병렬처리
1. 객체지향 프로그래밍 property & dataclass (p.139-140) @property - diameter 메서드는 사실 _radius * 2라는 계산을 수행하지만 외부에선 c.diameter라고 쓰면 바로 10이라는 결과를 얻을 수 있다. - @diameter.setter를 사용하면 c.diameter = 20 형태로 diameter을 수정할수있고 - 내부에서는 diameter을 받아 _radius=10으로 변환 저장한다. - fastapi에서 젤많이쓰는 기능이 속성화이다. @dataclass - 보통 클래스를 만들면 __init__으로 생성자, __repr__으로 객체 출력 형식, __eq__로 동등성 비교 등을 직접 정의해야 하는데 @dataclass를 붙이면 이런 메서드들이 자동 생성된다. - Point 클래스는 x, y 좌표만 Point(1,2)로 정의했는데 이 상태로 객체 p1, p2를 생성하고 출력하면 Point(x=1, y=2)처럼 형식맞춰 나온다. - 그리고 == 비교 시 자동으로 True도 나온다. @property - class Order - 주문 정보를 저장하는 클래스 - 속성: beverage, quantity - __slots__를 사용해 이 두 속성만 인스턴스에 저장할 수 있도록 제한했기 때문에 메모리 사용량이 줄고 실수로 다른 속성을 추가하는 것도 방지함. - total_price 메서드 - @property로 정의됨 - 주문 금액을 계산하는 로직을 담고 있지만 속성 접근처럼 쓸 수 있다 즉 order.total_price()가 아니라 order.total_price로 쓸수있다. cf) @property 안썼으면? - @property 사용하면 order.total_price로 괄호 없이 접근했을때, 내부에서 계산된 결과가 바로 반환되고 - @property 사용 안하면 order.total_price()로 호출하면 6000이 나오고 괄호없이 호출하면 메서드 객체 참조만 나온다. @dataclass - class Beverage - 음료 정보를 저장하는 데이터 전용 클래스 - 속성: name(문자열), price(실수형), tags(문자열 리스트) - @dataclass로 자동으로 `__init__`(생성자), `__repr__`(객체를 보기 좋게 출력), `__eq__`(값 비교) 같은 기본 메서드가 생성. cf) @dataclass 안썼으면? - `__init__` : 매개변수를 받아 속성을 초기화 / `__repr__` : 객체를 보기 좋게 문자열로 표현 / `__eq__` : 객체 간 동등성 비교 로직 작성 이렇게 하나하나 추가해야한다. decorator & closer (p.168-169) decorator - 데코레이터 (timer) - 함수 실행시간을 자동으로 측정 - 내부에 wrapper 함수를 정의해서 slow function을 감싼다. - 흐름 - wrapper는 시작시간기록, slow function 실행결과를 result에 저장하고 종료시간 기록, 걸린시간 계산, result를 반환 - @timer -> slow function을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 slow function이 호출 -> 2초 대기 -> 작업완료 출력 -> 실행시간 result 출력 - 의의 - 함수를 호출하기 전후에 원하는 로직을 끼워 넣어 원래 함수의 기능은 그대로 두고 부가적인 기능을 쉽게 추가할 수 있게. closure - outer()가 실행되면? - x = 10이 만들어지고 inner 함수가 정의됨 - outer()는 inner 함수를 그 자체로 반환함 (inner의 결과를 반환하는게 아니고) - closure = outer()? - closure에 **inner 함수**가 저장 - 이때 inner 함수는 자신이 정의될 당시의 환경(= x=10이 있던 outer의 스코프)을 함께 기억함 - 그래서 outer가 끝나서 x 변수가 사라진 것처럼 보여도 closure()를 실행하면 여전히 x = 10에 접근 가능. - closure는 decorator처럼 @문법을 붙이지 않아도 적용된다. decorator와 closure 함께사용하기 decorator - 데코레이터 measure_time - 실행 시간 측정 - 내부에 wrapper 함수를 정의해서 run_typed을 감싼다. - 흐름 - wrapper는 run_typed 실행결과를 elapsed_time에 저장한 뒤 반환 - @measure_time -> run_typed을 호출하면 사실상 wrapper가 실행된다. wrapper 안에서 run_typed가 호출 -> 실행시간 elapsed_time 출력 closure - measure_time이 실행되면? - wrapper 함수가 정의됨, measure_time은 wrapper 함수를 그 자체로 반환함 (wrapper의 결과를 반환하는게 아니고) - run_typed에 @measure_time이 적용되면? - run_typed 함수 객체가 measure_time의 매개변수 func로 전달 - measure_time 안에서 정의된 wrapper 함수는 자신이 정의될 당시의 환경(자기 바깥 함수의 지역 변수인 func)를 기억 - 그래서 measure_time이 종료되어 원래 지역 변수 func가사라진 것처럼 보여도 wrapper 함수 내부에는 여전히 func에 대한 참조가 살아 있다. 2. 병렬처리 multithreading (p.189) - 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유 - 예제 코드 - print_numbers와 print_letters를 각각 thread1 thread2로 실행 - 결과 - 숫자 1부터 5까지와 알파벳 A부터 E까지가 1초 간격으로 번갈아 출력 mutliprocessing (p.191) - 함수가 완전히 독립된 프로세스로 실행 - 예제 코드 - print_numbers와 print_letters를 독립적인 프로세스 process1 process2로 실행 - 결과 - 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위, 프로세스 생성 시점의 지연 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고 - 그 결과 숫자 1-5를 전부 찍고 난 후 알파벳 A-E를 찍는 식으로 출력이 묶음 단위로 나타난다. multithreading & mutliprocessing multithreading - 두 스레드가 같은 프로세스 내부에서 실행되며 메모리와 실행 환경을 공유한다. - 예제에서 숫자를 찍는 함수와 알파벳을 찍는 함수 각각이 독립적인 스레드로 동작하지만 동일한 프로세스의 GIL(Global Interpreter Lock)을 공유하기 때문에 한 번에 한 스레드만 실제로 파이썬 바이트코드를 실행한다. - time.sleep(1)로 실행 권한을 번갈아 준 결과 숫자를 하나 찍고 잠시 멈춘 사이 다른 스레드가 알파벳을 찍는 식으로 출력이 교차되고 실행 타이밍에 따라 순서가 조금씩 섞여 나타난다 즉 두 작업이 거의 동시에 진행되는 것처럼 보이지만 사실은 GIL과 sleep 호출에 의해 미세하게 번갈아 실행된다. mutliprocessing - 각 함수가 완전히 독립된 프로세스로 실행된다. - 두 프로세스가 동시에 시작되더라도 실행 타이밍과 OS 스케줄링 우선순위 때문에 한 프로세스가 먼저 실행을 많이 진행하고 다른 프로세스가 뒤따라 실행되게되고 그 결과 숫자 1-5를 전부 찍고 난 후에 알파벳 A-E를 찍는 식으로, 출력이 묶음 단위로 나타나게 된다. ~*사실잘모르겟다...어렵다,,,,*~ 결론 - 멀티스레딩은 하나의 프로세스 안에서 협력적으로 실행을 나누기 때문에 출력이 교차되거나 순서가 섞이기 쉽고, 멀티프로세싱은 프로세스 단위로 완전히 병렬 실행되지만 OS 스케줄링 특성상 한쪽이 먼저 실행을 마쳐 출력이 블록처럼 모이는 경우가 많다. MutClust에서 mutliprocessing 코드 MutClust 예전 utils 코드중에서 병렬처리 코드 있었던거같아서 찾아봣다 흐름은 1. multiprocessing.Pool을 이용해 최대 50개의 프로세스를 동시에 실행할 수 있도록 풀을 생성 2. target_dir 디렉토리 내 파일이 meta_df의 인덱스 이름에 포함되어 있는 경우만 남겨서 mutInfo_files 생성 3. mutInfo_files를 pool.map(process_mutInfo, mutInfo_files)에 전달 - process_mutInfo: 병렬로 process_mutInfo 함수에 의해 처리(mutInfo_files를 읽고 sid, mutInfo_df 생성) 4. key가 sid, 값이 mutInfo_df인 딕셔너리 seq_dict로 만들고 total_df로 정리 결론 - 변이 정보를 병렬 프로세스(50개)로 빠르게 처리하고 결과를 df로 정리해서 저장해놓고 썼다. - 결과파일 저장해놓은뒤로 사용한적없어서 utils에서 빠진거같고 기억에서도 빠진것같다(..)
#SKALA #python
2025-08-12 ⋯ python #1 기본문법, 가상환경, 로깅
1. 기본문법 break와 continue의 차이 (p.29) - break - 0부터 9까지 세는 반복문에서 i가 5가 되는 순간 break를 만나면 그 뒤의 숫자는 전혀 세지 않고 반복이 끝난다. - continue - 0부터 4까지 세는 반복문에서 i가 2인 경우 continue를 만나면 2를 출력하지 않고 바로 다음 숫자인 3으로 넘어가고 반복문 자체는 끝나지 않는다. 가변 인자 (p.78) - *args는 인자들을 하나의 튜플로 묶어서 받는다. - **kwargs는 인자들을 하나의 딕셔너리로 묶어서 받는다. - `mix_example(a, b, *args, **kwargs)`일때 `mix_example(1, 2, 3, 4, 5, name="철수", age=30)` - 1과 2는 매개변수 a와 b에 저장 - a:1, b:2 - 3, 4, 5는 args라는 튜플에 저장 - args: (3,4,5) - name="철수"와 age=30은 kwargs라는 딕셔너리에 저장 - kwargs: {'name':'철수', 'age':30} 클로저 (p.86) 1. multiplier(2)를 호출 2. factor가 2로 고정된 multiply 함수가 만들어짐. 3. 이 함수는 나중에 호출해도 2라는 값을 기억하고 있다. 4. double(10)을 하면 10에 2를 곱한 20이 나온다. 2. 가상환경 지금 환경을 그대로 뜨는 방법 (p.109) - pip freeze > requirements.txt를 하면 현재 환경에 설치된 모든 패키지와 그 버전이 기록되고 - 다른 환경에서 똑같은 설정을 만들고 싶다면 pip install -r requirements.txt를 실행하면 된다. .env (p.115) - 데이터베이스 비밀번호나 API 키처럼 코드에 직접 적으면 안 되는 값들은 .env라는 파일에 따로 저장하고 코드에서는 이 파일을 읽어서 사용하는 것이 안전하다. - 사용법 - from dotenv import load_dotenv로 불러오고 - load_dotenv()를 실행하면 .env 파일 안의 값들이 환경 변수로 등록된다. - os.getenv("DB_USER")로 필요한 값을 꺼낼 수 있다. - 깃허브에 올릴때는 gitignore에 넣어야된다. 3. Logging 실습 문제 .env를 위와같이 작성했을때 app.log에 다음 로그 출력하기 - INFO 레벨 메시지: "앱 실행 시작" - DEBUG 레벨 메시지: "환경 변수 로딩 완료" - ERROR 레벨 메시지: ZeroDivisionError 예외 발생 시 출력 코드 결과 app.log
#SKALA #python
2025-08-11 ⋯ Devops #1 Python 프로젝트 CI/CD & 클라우드 빌드
실습 - 메이크파일, 린팅, 테스트와 같이 파이썬 프로젝트 스캐폴딩에 필수적인 요소가 포함된 깃허브 저장소를 생성해보자. 그리고 간단하게 코드 포매팅을 수행하도록 메이크파일 스크립트를 작성해보자. - 깃허브 액션을 사용하여 두개 이상의 파이썬 버전에 대해 깃허브 프로젝트 테스트를 수행해보자. - 클라우드 네이티브 빌드 서버(AWS 코드빌드, GCP 클라우드 빌드, 애저 DevOps 파이프라인)를 사용하여 지속적 통합을 수행해보자. - 깃허브 프로젝트를 도커 파일로 컨테이너화하고, 자동으로 컨테이너 레지스트리에 새로운 컨테이너가 등록되도록 만들어보자. - locust 또는 loader io와 같은 부하 테스트 프레임워크를 사용하여 애플리케이션에 대한 간단한 부하 테스트 코드를 작성한다. 그리고 스테이징 브랜치에 변경 사항을 푸시할 때 이 테스트가 자동으로 수행되도록 만들어보자. 1. 파이썬 프로젝트 스캐폴딩 + 메이크파일/린팅/테스트 + 포매팅 새 프로젝트 만들기 (로컬) 최소 패키지/테스트 코드 넣기 개발 도구 설치 파일 - ruff(린터+포매터), pytest(테스트), mypy(타입체크)만 사용 메이크파일 작성 (포매팅/린팅/테스트 일괄 실행) 의존성 설치 & 동작 확인 깃허브 저장소 만들고 푸시 2. 여러 파이썬 버전으로 GitHub Actions 테스트 리포지토리 준비 브랜치 생성 워크플로우 폴더 만들기 CI 설정 파일 생성 - 파일 경로: .github/workflows/ci.yml - 내용: 매트릭스로 3.9~3.12 테스트, Makefile 타깃 사용 커밋 & 푸시 PR 생성 - GitHub에서 ci-setup → main으로 Pull Request 생성 - PR이 생성되면 Actions 탭에서 파이썬 3.9/3.10/3.11/3.12 네 개 잡이 병렬로 도는 걸 볼 수 있음. 배지 추가 - README.md에 아래 한 줄 추가(리포지토리 경로는 본인 것으로 교체) 1. Secrets 불필요: 단순 테스트만 하면 깃허브 액션 기본 권한으로 충분. 2. 기본은 ubuntu-latest지만, OS 매트릭스를 늘리고 싶으면 다음과같이 설정 3. Makefile 없이도 가능. 위 Lint/Type/Test 단계를 ruff/mypy/pytest 직접 실행으로 바꿔도 동작. 3. 클라우드 네이티브 빌드 서버로 CI 1. AWS CodeBuild 리포에 buildspec 추가 - 리포 루트에 buildspec.yml 생성 - 여러 파이썬 버전을 돌리고 싶다면 CodeBuild 프로젝트를 버전별로 2~3개 만들거나, Docker 이미지를 바꿔 실행하는 별도 프로젝트를 추가하는 방식이 단순함. CodeBuild 프로젝트 만들기(콘솔) 2. GCP Cloud Build 리포에 cloudbuild.yaml 추가 트리거 연결 cf) cloudbuild.yaml 예시(루트에 있어야 함) cf2) 여러 파이썬 버전으로 돌리기 – 두 가지 방법 1. 스텝을 여러 개 두기 2. 트리거를 2개 만들고, 각각 Substitution으로 버전 넘기기 - 트리거1: _PY_VERSION=3.9 - 트리거2: _PY_VERSION=3.12 - cloudbuild.yaml에서 ${_PY_VERSION} 사용: 3. Azure DevOps Pipelines 리포에 buildspec 추가 - 리포 루트에 azure-pipelines.yml 생성 Azure DevOps 파이프라인 생성 cf) - 만약 GitHub 권한 에러가 나면? - 좌측 하단 Project settings → Service connections → New service connection → GitHub → Grant access(또는 OAuth) → 연결 생성 - 다시 Pipelines → Create pipeline부터 진행 - 이미 리포에 있어야 하는 파일 예시 - azure-pipelines.yml (다중 파이썬 버전 매트릭스)
#SKALA #Devops
2025-08-01 ⋯ Docker #2 작년 작업 복기: netmhcpan image 불러와서 패키지 돌리기
2024.11.24 MutClust 작업중에 netmhcpan을 돌려야되는 상황이 왓었는데 netmhcpan이 유료였나 그래서 패키지 다운은 안되고 담당 박사님은 그만두셧고.. 서버 뒤지다가 위 README 파일 발견해서 결과물 저장까진 했던 기억이있다. 이때먼가 의문이 들었던게 새로운 conda 환경에 접속한거같은 느낌이 아니라 완전 다른 제2의서버에 접속한 느낌이었는데 이상하게 연구실 디렉토리들은 그대로 접근이 가능해서 혼란스럽지만 그냥 절대경로 다 박고 수행했는데 결과들이 문제없이 저장됐었다. 그래서 그뒤로 걍잊어버리고있었는데 docker 배우고나니까 먼가 이해돼서 이해된김에 정리해보기! 2. 도커 이미지 pull 및 수행 1. 도커 이미지 다운로드 당시 chatgpt 응답 > `docker pull * (*: 링크 블라인드 처리)` 명령어는 Docker Hub에서 *의 1.0 버전을 다운로드합니다. 이미지 크기가 953.76 MB이므로, 다운로드에는 네트워크 속도에 따라 몇 분 정도 소요될 수 있습니다. > 2. 도커 이미지 수행 당시 chatgpt 응답 > 이미지 다운로드 후, 다음과 같은 명령어로 Docker 컨테이너를 생성하고 실행할 수 있습니다. 예를 들어, 로컬 시스템의 특정 디렉토리를 컨테이너에 마운트하고 싶다면: > > `docker run -it -v /path/to/local/datac:/data *` > > 이 명령은 다음과 같은 작업을 수행합니다: > > - `it`: 컨테이너를 대화식 모드로 실행하고 터미널 입력을 위한 인터페이스를 제공합니다. > - `v /path/to/local/data:/data`: 호스트 시스템의 `/path/to/local/data` 디렉토리를 컨테이너의 `/data` 디렉토리에 마운트합니다. 이렇게 하면 컨테이너 내부에서 호스트 시스템의 데이터에 접근할 수 있습니다. 당시 내 코드 몰랐는데 호스트 시스템(우리 서버)의 내 작업 디렉토리("/data/home/ysh980101/2411/data-mhc")를 컨테이너에 마운트 했었다. 3. netMHCpan 실행 1. netMHCpan 설치 경로 찾기 당시 chatgpt 응답 > > 컨테이너가 시작되면 기본적으로 `/home/RSEM-1.3.3` 디렉토리에서 시작되는 것 같습니다. 이 디렉토리는 RSEM 관련 작업을 위한 것으로 보이며, netMHCpan 작업을 수행하려면 적절한 디렉토리로 이동해야 할 수 있습니다. > > 컨테이너 내에서 netMHCpan 설치 디렉토리로 이동하려면, 해당 디렉토리의 경로를 알아야 합니다. netMHCpan의 설치 위치를 확인하고, cd 명령어로 그 위치로 이동합니다. > > `cd /path/to/netMHCpan` 당시 내 코드 netMHCpan의 설치 디렉토리 /netMHCpan-4.1 를 찾음. 2. netMHCpan 사용 당시 chatgpt 응답 > 디렉토리로 이동: 이제 확인된 설치 경로로 이동하여 작업을 시작합니다. > > `cd /netMHCpan-4.1` > > 작업 실행: netMHCpan을 실행하려면, 해당 디렉토리 내에서 실행 파일을 찾아 해당 명령을 사용합니다. 예를 들어, 다음과 같은 명령어를 사용할 수 있습니다: > > `./netMHCpan -a HLA-A02:01 -l 9 -f your_peptide_file.txt -o output.txt` > > 이 명령은 HLA-A02:01 앨리얼을 대상으로 하는 9-mer 펩타이드를 your_peptide_file.txt에서 읽어들여 output.txt에 결과를 출력합니다. 당시 내 코드 patient_id.txt에 있는 각 환자 ID에 대해 루프를 돌림 환자별 .csv 파일에서 첫 번째 컬럼(펩타이드 서열)만 추출하고, 헤더를 제거해서 .txt 파일로 만들고 Affinity prediction하려는 hla allele를 HLA_I_ALLELES_FILE에 저장해놧엇는데 HLA_I_ALLELES_FILE에 대해 루프를 돌림. 각 allele마다 NetMHCpan 실행 결과를 allele 이름으로 된 .txt 파일로 저장. 해당 환자의 모든 allele에 대한 결과 파일을 하나의 .csv로 병합. sc.py는? 대충 이런식인데 최종적으로는 Allele,Peptide,Affinity 컬럼 갖는 테이블을 반환. 전체 bash script는 이랬다 4. Docker 종료 그때 노션 보니까 챗지피티가 이런말도 해줫다. > 작업이 완료되면, exit 명령어를 입력하여 컨테이너에서 나올 수 있습니다. 컨테이너를 종료하지 않고 나온 경우, 다음과 같이 컨테이너를 다시 시작하거나 종료할 수 있습니다. > > 컨테이너 재시작: `docker start [container_id_or_name]` > > 컨테이너 내부로 들어가기: `docker attach [container_id_or_name]` 이때 이해를못한상태니깐 exit를 하면 그냥 완전 나가기가 된다고 생각했던거같다. 그래서 한 10번 넘게 들어가서 작업했는데 내가 컨테이너를 하나도 종료안해놔서 한 6개월뒤에 사람들이 ys910111 누구냐고 머라했던기억이 ㅋㅋ ㅠㅠ 그래도 이제 먼가 이해되니깐조은듯.
#Docker
2025-08-01 ⋯ Docker #1 Python 실행 컨테이너 만들기
Background - RDE Local PC에서 RDE 환경 구성에서 Harbor registry로부터 RdE Container download를 수행했음 - 아이콘을 클릭해서 RDE 런처를 실행한다. 1. 웹 서비스 실행 컨테이너 만들기 1. /config/workspace/cloud/container/00.container-linux 경로로 이동 2. 디렉토리 구조는? 3. Dockerfile 내용 확인하기 내용은? - Alpine 리눅스를 기반으로 Python 3.11 환경 사용 - Python 빌드에 필요한 기본 패키지 설치 (bash, curl, gcc, musl-dev, linux-headers, jq) - Python 패키지 설치 (fastapi uvicorn psutil python-multipart) 그리고 "/mycode"를 볼륨으로 지정. - webserver.py를 컨테이너 내 디렉토리 /mycode 아래에 복사하기 (-> 컨테이너가 종료되더라도 그대로 남아있게) - 웹 서버를 무한 대기 상태로 유지 (컨테이너가 실행되었을 때 바로 종료되지 않고 계속 살아있도록) 2. Docker 이미지 빌드 3. mydata에 webserver.py 복사 후 실행 1. 현재 파일 구조 내부 확인해보면? 비어있음. 2. 다른 터미널 열어서 복사해주고 확인 3. 수행후 파일 구조 4. webserver.py 수행 하면 "http://localhost:8888/login"에서 welcome page가 뜬다. 4. 컨테이너를 실행 즉시 webserver가 되도록 해보자 1. Dockerfile 내용 수정 컨테이너가 실행되면 FastAPI 서버가 자동으로 실행되게함 2. 컨테이너 실행 하면 "http://localhost:8888/login"에서 welcome page가 뜬다. 3. 아까랑 다른점? 아까는 `python /mycode/webserver.py` 해야 떴는데 이제는 바로뜬다는 차이가있음. 5. 의문점 정리 1. RDE 아이콘 클릭하는것의 의미는? RDE 런처를 수행하면 사전에 정의된 Docker 컨테이너를 자동으로 구동한다. 이 컨테이너는 실습에 필요한 파일, 디렉토리, 개발 환경 설정, 코드 파일, 그리고 VS Code 서버를 포함한 완성된 개발 환경이 포함된 이미지로 구성되어있음. 2. /config/workspace/cloud/container/00.container-linux와 같은 경로에 접근 가능한 이유는? 컨테이너 이미지 안에 이미 내장되어 있어서 컨테이너가 실행됨과 동시에 해당 경로가 자동으로 구성된다. 즉 디렉토리를 다운로드했다는 개념보다는 컨테이너 실행과 동시에 그 안에 포함된 실습 디렉토리가 자동 마운트되었다고 이해하는 것이 더 정확하다. 3. 볼륨으로 지정? Docker 컨테이너는 기본적으로 임시 환경이기 때문에, 컨테이너 내부에서 생성한 파일이나 수정한 내용은 컨테이너가 삭제되면 모두 함께 사라진다. 하지만 VOLUME 명령을 통해 지정된 경로는 영속적인 볼륨으로 연결되며, 컨테이너가 삭제되어도 해당 경로에 저장된 데이터는 유지된다. "/mycode"를 볼륨으로 지정하고 컨테이너 내부에서 /mycode 경로에 파일을 생성하거나 수정하면 그 경로가 호스트 시스템의 익명 볼륨에 저장되기 때문에 컨테이너가 종료되더라도 그대로 남아 있게 된다.
#SKALA #Docker
2025-07-31 ⋯ SQL #6 AI 서비스 리뷰 시스템
1. 문제 AI 서비스 리뷰 시스템: 키워드 기반 텍스트 필터링과 AI 기반 방식의 비교를 통해 유사도 기반 검색에 대한 개념 이해 - 테이블 개요 - Day 3 – ai_service_creator_ranking.sql - 주제: AI 서비스 리뷰 (WITH (CTE) + 집계로 인기 기획자 추출) - 목적: CTE(Common Table Expression)로 집계 테이블을 구성, AVG(평점)과 COUNT(리뷰)를 기준으로 인기 있는 기획자 선정, ROW_NUMBER()로 랭킹 부여, 향후 AI 추천(예: 유사도 기반 + 평점 기반 추천) 전단 필터링에 활용 - 실습 문제 - 아래의 실습에 대해 각각 SQL문 + 결과 화면 Capture + 성능 분석결과 + 속도 차이에 대한 원인을 정리하셔서 제출하세요. - 각 기획자의 평균 평점과 리뷰 수를 계산하고, 리뷰 수가 2개 이상인 사람 중에서 평점이 높은 순으로 랭킹 정리 - 최소 쿼리를 2개 이상 작성하고 각각에 대한 실행결과값이 어떻게 나오는지 비교하여 원인에 대한 의견 정리 2. 데이터 생성 3. 쿼리 수행하고 실행결과 확인 1. CTE + ROW_NUMBER() - 설명 - 소요 시간: 3.7 ms - ROW_NUMBER 순위 부여를 통해 추천 우선순위를 생성 - AI 확장성 o - AI 응용 예시 | 구성요소 | 설명 | | --- | --- | | creator_stats | 평균 평점 + 리뷰수로 인기 기획자 후보군 필터링 | | ROW_NUMBER() | 상위 N명의 기획자 순위화하여 추천 순서 정렬 | | AI 연계 | 좋은 평가 순으로 상위 N명을 추려 벡터 유사도 필터에 결합해서, 추천 우선순위를 정해주는 전처리용 순위 테이블로 사용 | 2. 서브쿼리 + ORDER BY - 설명 - 소요 시간: 1.48 ms - 빠르지만 순위 컬럼이 없음 - AI 응용 예시 | 구성요소 | 설명 | | --- | --- | | 서브쿼리 | 집계 후 리뷰수 ≥ 2 필터링, 평점순 정렬 | | ORDER BY | 순위 부여 없이 정렬만 수행 | | AI 연계 | 유사도 추천 이전에 단순 평점 정렬 필터로 사용 가능 | 3. RANK() - 설명 - 소요 시간: 1.35 ms - RANK는 동점 처리 가능 - AI 확장성 o - AI 응용 예시 | 구성요소 | 설명 | | --- | --- | | creator_stats | 리뷰 수 + 평균 평점 기준으로 필터링된 기획자 집계 | | RANK | 평점 기준 동점순위 허용 → 보다 유연한 랭킹구조 제공 | | AI 연계 | 동점 순위를 허용해 같은 우선순위의 여러 추천 후보를 제공 가능 → 유사도 추천 결과와 합쳐서 유연하게 순위 적용 가능 | 4. FILTER() - 설명 - 소요 시간: 1.05 ms - 가장 빠른 쿼리, 리뷰가 없는 기획자도 분석 가능 - AI 확장성 o - AI 응용 예시 | 구성요소 | 설명 | | --- | --- | | FILTER() | 조건부 집계를 통해 빠르게 평점 평균 계산 | | LEFT JOIN | 리뷰가 없는 기획자까지 포함하여 전체 후보군 생성 가능 | | AI 연계 | 실시간 추천이나 전체 기획자 간 유사도 비교를 빠르게 할 수 있다. 또한 리뷰가 없더라도 모든 기획자 정보를 포함해서 추천 후보에 넣을 수 있다. | 4. 성능 비교 쿼리1 vs 쿼리2 쿼리1은 랭킹 컬럼을 제공하므로 상위 N명을 추출하거나 사용자가 현재 몇 위에 있는지를 알려주는 추천 시스템에서 유리하다. 하지만 성능 측면에서는 다소 비용이 든다. 쿼리2는 순위를 부여하는 컬럼이 없기 때문에 추천 알고리즘에서 특정 위치를 식별하거나 상위 몇 명을 구분하는 데는 추가 처리 또는 래퍼 함수가 필요하지만, 성능은 빠르다. 쿼리1 vs 쿼리4 쿼리 1은 조인된 리뷰 데이터를 기준으로 필터링과 정렬, 순위까지 모두 수행하며 리뷰가 없는 기획자는 전혀 포함되지 않는다. 이에 비해 쿼리 4는 LEFT JOIN을 통해 리뷰가 존재하지 않는 기획자까지 포함하고, FILTER() 구문으로 조건부 집계를 수행한다. 이로 인해 전체 기획자에 대한 벡터 기반 유사도 분석에 활용하기 유리하다. 또한 Postgres 전용 함수를 사용해서 실행 시간과 Planning 시간이 빠른 편이다. 쿼리1 vs 쿼리3 쿼리1의 ROW_NUMBER()은 단순히 정렬된 순서대로 1, 2, 3... 순위를 부여하는 반면, 쿼리3의 RANK()는 동점 처리 시 동일한 순위를 부여하고 그 다음 순위를 건너뛴다. 예를 들어, 동일한 평점이 2개 있다면 ROW_NUMBER()는 각각 1, 2로 부여하고, RANK()는 둘 다 1로 부여한 뒤 다음은 3이 된다. 성능 측면에서 RANK()는 ROW_NUMBER()보다 처리량이 적다. ROW_NUMBER()는 모든 행을 고유하게 구분해 정렬해야 하지만, RANK()는 동점 처리를 허용하기 때문에 정렬 이후 중복값을 묶는 처리를 덜 수행하고 실제 성능도 쿼리3이 더 빠르다. 또한 기능 측면에서 기능적으로도 동일 평점을 받은 기획자를 "동일 순위"로 처리하는 구조이기 때문에 RANK()는 사용자에게 더 유연한 결과를 제공할 수 있다.
#SKALA #SQL
2025-07-30 ⋯ SQL #4 AI 피드백 분석 시스템의 테이블 정규화
1. 문제 AI 피드백 분석 시스템의 테이블 정규화 - 시나리오 - 여러분은 AI 피드백 분석 시스템을 위한 데이터 모델링을 맡았습니다. - 현재는 여러 실험 데이터를 한 테이블에 모아두었지만, 벡터 임베딩 처리, 학습데이터 전처리, RAG 문서 기반 검색 등을 고려해 정규화 설계가 필요합니다. [비정규 테이블 예시: Day 2 – 정규화와 제약조건_실습1_예제_ai_feedback_raw.csv] - 실습 목표 - LLM Feedback 데이터 정규화 (3NF까지 고려) - model, user, prompt-response, tags 분리 - tags 필드는:TEXT[ ] 배열로 유지한 구조 (빠른 전처리, FAISS 등 용이) - feedback_tag라는 별도 테이블로 정규화 (통계, RAG 전처리 유리) - AI 분석 목적의 전처리 성능 관점에서 두 방식 비교 설명 2. Objective - 비정규화된 AI 피드백 테이블을 정규화 - 사용자, 모델, 질문-응답(prompt-response), 태그(tags) 정보를 분리 - 태그를 배열 형태(TEXT[]), 또는 별도 정규 테이블(feedback_tag)로 관리하여 AI 분석 목적(임베딩, 전처리, RAG)에 적합한 구조로 테이블 설계 3. AI 피드백 테이블 확인 원본 데이터를 확인해보면 아래와같고 정규화할 대상과 정규화방법은 아래와 같다. - model: 모델 정보 테이블로 분리 (models) - user_id, user_name: 사용자 테이블로 분리 (users) - prompt, response: 피드백 본문 테이블로 분리 (feedbacks) - tags: 별도 테이블로 정규화( 배열 유지 방식 + 연결 테이블 (feedback_tags)) - created_at: feedbacks 테이블에 포함 4. 테이블 정규화 3NF까지 고려해서 정규화하기. 정규화 후 데이터 삽입하기 5. ERD 작성 dbdiagram 사용 https://dbdiagram.io/ 구조 dbdiagram으로 ERD 작성 6. AI 분석 목적의 전처리 성능 관점에서 두 방식 비교 설명 (TEXT[] 배열로 tags를 저장하는 방식 vs tags를 별도 테이블(feedback_tag)로 정규화하는 방식) 먼저 TEXT[] 배열로 tags를 저장하는 방식은 한 피드백에 대한 여러 태그 정보를 하나의 행에 함께 저장하는 구조인데 예를 들어 어떤 사용자 피드백이 "positive", "concise", "creative"라는 태그를 갖는다면, 이 세 단어를 배열로 묶어 하나의 셀에 저장합니다. 이 방식의 장점은 빠른 접근성과 효율적인 처리 속도입니다. LLM 기반 피드백 시스템에서는 종종 전체 텍스트나 임베딩을 이용한 벡터 검색(ex. pgvector)을 수행하는데 이때 태그 정보가 같은 행에 묶여 있으면 텍스트 단위 처리 또는 배치 임베딩에 용이하고 특히 모델 학습이나 벡터 임베딩 시 태그 정보를 문맥 정보로 같이 넘겨야 하는 경우 이 구조는 파이프라인 단순화에 큰 도움이 됩니다. 하지만 태그 단위로 집계하거나 통계 분석을 하고자 할 경우 배열 내부 요소를 하나하나 파싱하거나 unnest() 같은 SQL 함수로 분리해 처리해야 하는데 예를 들어 "어떤 태그가 가장 자주 사용되었는가?"라는 질문을 하려면 배열에서 모든 태그를 추출하고 세는 별도 과정이 필요합니다. 반대로 tags를 별도 테이블(feedback_tag)로 정규화하는 방식은 각 태그를 하나의 행으로 저장하고 피드백 ID와 연결하는데 이때 feedback_tag 테이블은 "feedback_id - tag" 형태로 구성되며, 각 피드백에 여러 태그가 있을 경우 그 수만큼의 행이 생성됩니다. 이 방식의 가장 큰 장점은 쿼리 처리에서의 유연성입니다. 앞서 언급한 "가장 많이 쓰인 태그"나 "특정 태그가 달린 피드백 목록"을 매우 쉽게 쿼리할 수 있습니다. 그러나 데이터가 다소 늘어난다는 단점도 있습니다. 예를 들어 100개의 피드백에 평균 4개의 태그가 달려 있다면, feedback_tag 테이블에는 400개의 레코드가 추가로 생기고 피드백을 조인하여 조회하는 경우 JOIN 연산의 비용이 듭니다. 이로 인해 벡터 임베딩이나 배치 학습 시에는 조인을 반복해야 하므로 배열 기반보다 느릴 수 있습니다. 결론적으로, 태그를 배열로 유지하는 방식은 벡터 기반 검색, 임베딩 처리, LLM 학습에 적합하고 시스템의 입력-출력 속도를 높이는 반면, 태그를 별도 테이블로 정규화하는 방식은 태그 관리 및 유지 보수에 유리합니다.
#SKALA #SQL
2025-07-29 ⋯ SQL #3 스키마 분리와 AI 분석
생각 정리 1. AI 분석이 들어갈 때 왜 별도 스키마로 나누는 것이 유리할까요? 2. 스키마 vs. 테이블 분리, 어떤 방식이 어떤 상황에 적합할까요? 3. 향후 pgvector 또는 AI 모델 결과를 넣기 위해 어떻게 테이블을 확장할 수 있을까요? 1. AI 분석이 들어갈 때 왜 별도 스키마로 나누는 것이 유리할까요? AI 분석이 포함된 시스템에서 데이터를 다룰 때, 별도 스키마로 나누는 것이 유리한 이유는 (1) 데이터의 사용 목적이 다르기 때문이고, (2) 데이터의 구조와 속성이 근본적으로 다르기 때문입니다. 먼저 데이터의 사용 목적이 다르면 별도의 스키마로 운영하는것이 유리합니다. 예를 들어, 우리가 학교에서 수업을 들을 때 쓰는 노트와, 친구와 놀러 갈 계획을 적는 다이어리는 서로 내용도 다르고, 사용 목적도 다르기 때문에 같은 공책에 막 섞어 쓰면 나중에 헷갈리고 찾기 어렵습니다. 데이터베이스도 마찬가지인데, 예를 들어 수강생(Student), 강사(Instructor), 수강신청(Enrollment) 같은 테이블은 대부분 운영 데이터를 담고 있고 실제 서비스가 돌아가기 위해 실시간으로 쓰이고 조회됩니다. 그런데 AI 분석에 사용되는 데이터, 예를 들면 수강 리뷰의 임베딩 값(Embedding)이나 학생 행동 로그에서 추출한 패턴 정보는 운영 목적이 아니라 분석 목적입니다. AI 분석 데이터는 실시간보다는 주기적으로 갱신되고, 대량의 수학적 계산을 거쳐 만들어지며, 사용자가 직접 보는 게 아니라 모델이 참고합니다. 그러니까 이 두 데이터를 같은 공간에 두는 건, 수업 노트 옆에 여행계획을 적는 것과 같이 찾기도 어렵고, 실수도 생기고, 결국 혼란을 유발할 수 있습니다. 두 번째로 데이터의 구조와 속성이 다른 경우 별도의 스키마로 운영하는것이 유리합니다. 운영 데이터는 일반적으로 사람이 입력한 명확한 값으로 구성됩니다. 예를 들어 이름, 전화번호, 수강과목 등은 짧고, 일정한 형식을 갖고 있다. 그런데 AI 분석을 위한 데이터는 길고 복잡한데, 예를 들어 학생이 쓴 리뷰를 BERT 모델로 임베딩하면 768차원의 벡터로 바뀌고, 이런 벡터는 숫자 덩어리이기 때문에 일반 SQL 쿼리로는 다루기 어렵습니다. 또 AI 분석에서는 반복 실험을 하거나 다양한 모델 결과를 저장해야 하므로, 새로운 컬럼이 자주 생기고 스키마 구조도 자주 바뀔 수 있습니다. 예를 들어 '학생 행동을 기반으로 예측된 이탈 위험 점수'나 '추천 과정 리스트' 같은 컬럼은 운영 데이터에서는 필요 없지만, 분석에서는 매우 중요한데, 이처럼 구조적으로 유연하고 실험적인 데이터를 기존 운영 스키마에 억지로 끼워 넣으면, 전체 시스템이 복잡해지고 에러도 많아집니다. 그래서 아예 AI 분석용 스키마를 따로 만들어 거기에 AI 전용 테이블을 모아두면, 운영 시스템은 안정성을 유지하면서도 분석팀은 자유롭게 데이터를 다룰 수 있습니다. 예를 들어 서울캠퍼스 학생 데이터를 담은 테이블이 `seoul.students`이고, AI 분석 결과로 얻은 학생 행동 임베딩이 `analytics.student_embeddings`에 저장돼 있으면, 운영 데이터는 수업 등록이나 점수 관리에 집중하고, 분석 스키마는 AI 모델의 입력 및 출력 데이터 저장에 집중합니다. 운영 스키마는 변경이 거의 없지만, 분석 스키마는 새로운 모델이 생길 때마다 컬럼이 추가되거나 테이블이 생길 수 있는데 서로 독립적이기 때문에 안정성과 유연성을 동시에 확보할 수 있습니다. 2. 스키마 vs. 테이블 분리, 어떤 방식이 어떤 상황에 적합할까요? 스키마와 테이블 분리는 둘 다 데이터베이스를 논리적으로 구분하고 정리하기 위한 방법인데, 접근 제어가 필요한 경우와 같은 구조이지만 맥락(도메인)이 다를 경우에 스키마 분리가 적합하고, 같은 도메인 안에서 구조나 의미가 다른 데이터를 함께 관리할 때는 테이블 분리가 적합합니다. 먼저 스키마는 일종의 '공간'입니다. 예를 들어, 회사에서 부서마다 각기 다른 문서를 관리한다고 가정해봤을때, 경영팀은 예산 파일, 인사팀은 사원 평가표, 개발팀은 코드 문서를 관리합니다. 이걸 한 폴더에 몰아넣으면 각 부서가 실수로 다른 부서 문서를 건드릴 수 있습니다. 대신 부서별 폴더를 따로 만들어 놓고 권한을 설정하면, 인사팀은 인사 폴더만 접근 가능하고 경영팀은 경영 폴더만 볼수있습니다. 데이터베이스에서 이 '부서별 폴더'가 바로 스키마입니다. 예를 들어 `seoul.students`, `jeju.students`처럼 캠퍼스별 학생 데이터를 스키마로 구분하면, 서울 캠퍼스 운영자는 `seoul`만 접근할 수 있게 만들고, 제주 캠퍼스 운영자는 `jeju`만 다루게 할 수 있습니다. 테이블 분리만으로는 특정 테이블에만 접근을 제한하기 어렵고, 관리가 복잡해질 수 있습니다. 두 번째로, 같은 구조이지만 맥락(도메인)이 다를 경우에도 스키마 분리가 좋다. 예를 들어, 대학의 학사 시스템이 있을 때 서울과 제주 두 캠퍼스가 있는데, 학생, 수강, 강의 리뷰 등의 테이블 구조는 동일하지만 운영은 독립적입니다. 이럴 때는 `seoul.enrollments`와 `jeju.enrollments`처럼 스키마로 구분하면 같은 종류의 데이터를 혼동 없이 관리할 수 있습니다. 반면, 만약 '서울학생'과 '제주학생'을 한 테이블 `students`에 `campus` 열을 추가해서 구분한다면, 운영이나 통계 측면에서 실수하기 쉽고, 특정 캠퍼스의 데이터만 쿼리하려면 매번 조건문을 붙여야 합니다. 즉, 스키마 분리는 구조는 같지만 실질적으로 분리된 독립 단위를 표현하는 데 유리합니다. 테이블로만 나누면 이런 맥락 구분이 흐릿해지고, 유지보수가 어려워질 수 있습니다. 반면 테이블 분리는 같은 도메인 안에서 구조나 의미가 명확히 다른 데이터를 함께 관리할 때 적합합니다. 예를 들어, 한 학사 시스템 안에서 `courses` 테이블은 개설된 과목 정보를 담고, `course_descriptions`는 그 과목에 대한 상세한 설명을 담는다고 하면 이 둘은 같은 "강의"라는 도메인에 속해 있지만, 정보의 종류와 목적이 다르기 때문에 하나의 테이블에 섞지 않고, 테이블을 분리해서 관리하는 것이 바람직합니다. `courses`와 `course_descriptions`처럼 성격이 다른 데이터를 스키마 분리로도 독립성 확보가 가능하긴 합니다. 그러나 둘다 둘 다 "강의"라는 도메인에 속하며 도메인은 동일하되 데이터의 성격만 다른, 이러한 경우에는 기능적 분리만 필요하지, 운영 주체나 보안 경계까지 분리해야 하는 것은 아닙니다. 성능 및 관리 효율성 측면에서 테이블 분리가 더 실용적이므로 테이블 분리 수준에서 멈추는 것이 더 적합합니다. 3. (스키마가 이와 같을 때) 향후 pgvector 또는 AI 모델 결과를 넣기 위해 어떻게 테이블을 확장할 수 있을까요? 향후 pgvector를 도입하거나 AI 모델의 예측 결과를 넣기 위해 테이블을 확장하려면 2가지를 고려해서 확장해야 합니다. 첫째, 벡터 임베딩과 같은 AI 결과물은 원본 데이터와 별도로 관리되도록 전용 테이블을 분리해 저장해야 합니다. pgvector는 고차원 벡터 데이터를 다루기 위한 PostgreSQL 확장 기능인데, 이건 보통 텍스트나 이미지, 수강 패턴 등과 같은 복잡한 데이터를 수치화한 결과물이다. 그런데 이걸 기존 테이블, 예를 들어 `courses`나 `students` 테이블 안에 `vector(768)` 같은 컬럼으로 그냥 넣어버리면, 일단 저장은 가능하지만 문제가 생기는데 하나의 테이블이 너무 많은 역할을 하게 되면 데이터의 의미가 혼재되고 AI 모델이 바뀔 때마다 갱신도 어렵고 이전 값과 새 값을 비교하기도 어려워집니다. 예를 들어 `courses` 테이블에 있는 `title`이 바뀌지는 않았는데, AI 임베딩 벡터만 업데이트하려면 전체 레코드를 다시 수정해야 하므로 비효율적입니다. 그래서 AI 관련 벡터는 따로 관리하는것이 효율적입니다. 예를 들어 `analytics.course_vectors`라는 테이블을 만들고 여기에 `course_id`, `vector`, `updated_at`이라는 컬럼만 두면 AI 임베딩의 저장과 갱신이 훨씬 단순해집니다 즉, 벡터 데이터를 넣고 싶다면 기존 테이블을 확장하는 것이 아니라, 벡터만 따로 저장하는 전용 테이블을 만들고, 필요한 ID만 외래키로 연결하는 게 가장 안정적이고 관리가 쉬운 방법입니다. 둘째, AI 결과와 원본 데이터를 명확히 연결해주는 참조 구조가 중요합니다. AI 분석은 결국 원본 데이터를 바탕으로 나온 결과물입니다. 그러니까 이 결과가 어떤 데이터에 기반해서 나왔는지를 명확히 추적할 수 있어야 합니다. 예를 들어 `student_embeddings`라는 테이블이 있다고 하면, 벡터값은 단순히 512차원짜리 수치 덩어리일 뿐인데, 이걸 어떤 학생을 표현한 것인지, 서울 캠퍼스인지 제주 캠퍼스인지, 언제 생성된 것인지 명확히 기록하지 않으면 나중에 분석이나 추천 시스템에 쓸 수 없습니다. 그래서 실제 테이블 구조는 이렇게 설계 가능합니다: `student_id`, `campus`, `embedding`, `updated_at`. 여기서 `student_id`는 기존의 `seoul.students` 혹은 `jeju.students`의 기본키와 매칭되고, `campus`는 데이터 출처를 명확히 하기 위한 메타데이터 역할을 합니다. 즉, 단순히 벡터만 저장하는 게 아니라, AI 결과가 어느 테이블의 어느 엔티티에 대응되는지를 명시적으로 외래키 또는 참조 메타데이터로 남겨야 한다는 점이 매우 중요합니다. 그래야만 벡터 기반 검색이나 추천 알고리즘을 구현할 때 “어떤 학생의 임베딩”인지, “어떤 리뷰의 감성 점수인지” 등을 정확히 추적할 수 있습니다. 예를 들어, 학생 리뷰(comment)를 기반으로 감정 분석 점수와 벡터를 저장한다고 하면 `analytics.review_embeddings`라는 테이블을 만들고, 여기에 `review_id(FK)`, `embedding`, `sentiment_score`, `updated_at` 컬럼을 만들면 이 구조는 리뷰와 AI 결과를 연결할 뿐 아니라 AI 분석이 언제 수행되었고 어떤 데이터를 기반으로 했는지를 명확히 하고 이는 향후 모델이 바뀌거나 벡터를 다시 계산해야 할 때 매우 중요한 기준이 됩니다.
#SKALA #SQL