0환경 설정
1
Ollama + Gemma —
ollama pull gemma2:2b (2B, ~1.6GB)2
패키지 —
pip install fastapi uvicorn transformers torch accelerate keras-nlp1경량 추론 서버 (Gemma 특화)
Gemma 2B는 4GB RAM에서도 실용적인 속도로 동작합니다. 엣지 서버나 저사양 환경에 최적입니다.
lightweight_server.py — 저사양 환경 최적화 서버
Gemma 2B의 낮은 메모리 요구량을 활용. 배치 처리와 캐싱으로 처리량 극대화.
from fastapi import FastAPI
from pydantic import BaseModel
from ollama import Client, AsyncClient
import asyncio
from functools import lru_cache
import hashlib
app = FastAPI(title="Gemma Lightweight Server")
sync_ollama = Client()
async_ollama = AsyncClient()
class InferenceRequest(BaseModel):
prompt: str
max_tokens: int = 256
use_cache: bool = True
model: str = "gemma2:2b" # 2B 기본값
# 응답 캐시 (동일 프롬프트 재사용)
_cache: dict[str, str] = {}
def get_cache_key(prompt: str, model: str) -> str:
return hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()
@app.post("/infer")
async def infer(req: InferenceRequest):
key = get_cache_key(req.prompt, req.model)
if req.use_cache and key in _cache:
return {"response": _cache[key], "cached": True}
resp = await async_ollama.generate(
model=req.model,
prompt=req.prompt,
options={"num_predict": req.max_tokens, "temperature": 0.7},
)
result = resp["response"]
if req.use_cache:
_cache[key] = result
return {"response": result, "cached": False}
# 배치 처리 (여러 프롬프트 동시)
@app.post("/batch")
async def batch_infer(prompts: list[str], model: str = "gemma2:2b"):
async def single(p: str) -> str:
r = await async_ollama.generate(model=model, prompt=p)
return r["response"]
# 동시에 최대 4개 처리
semaphore = asyncio.Semaphore(4)
async def limited(p: str) -> str:
async with semaphore:
return await single(p)
results = await asyncio.gather(*[limited(p) for p in prompts])
return {"results": list(results), "count": len(results)}
# 헬스체크 + 모델 상태
@app.get("/health")
def health():
models = sync_ollama.list()
available = [m["name"] for m in models.get("models", [])]
return {
"status": "ok",
"available_models": available,
"cached_responses": len(_cache),
}python
2문서 요약 파이프라인
summarizer.py — 계층적 요약
긴 문서를 청크로 나눠 각각 요약 후 최종 통합 요약 생성. 컨텍스트 제한을 우회하는 MapReduce 패턴.
from langchain_community.llms import Ollama
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain.prompts import PromptTemplate
llm = Ollama(model="gemma2", temperature=0.3)
# 요약 프롬프트
MAP_PROMPT = PromptTemplate(
input_variables=["text"],
template="다음 텍스트를 3~5문장으로 핵심만 요약하세요:\n\n{text}\n\n요약:"
)
REDUCE_PROMPT = PromptTemplate(
input_variables=["text"],
template="""다음은 긴 문서의 각 부분을 요약한 내용입니다.
이를 통합해 전체 문서의 완성된 요약을 작성하세요.
{text}
최종 요약 (불릿 포인트 형식으로):"""
)
def summarize_document(text: str, chunk_size: int = 1000) -> dict:
"""
긴 문서를 MapReduce 방식으로 요약합니다.
Returns: {"summary": str, "chunks": int, "method": str}
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=100,
)
docs = splitter.create_documents([text])
if len(docs) == 1:
# 짧은 문서는 직접 요약
chain = load_summarize_chain(llm, chain_type="stuff", prompt=MAP_PROMPT)
method = "direct"
else:
# 긴 문서는 MapReduce
chain = load_summarize_chain(
llm,
chain_type="map_reduce",
map_prompt=MAP_PROMPT,
combine_prompt=REDUCE_PROMPT,
)
method = "map_reduce"
result = chain.invoke({"input_documents": docs})
return {
"summary": result["output_text"],
"chunks": len(docs),
"method": method,
"original_length": len(text),
}
# FastAPI 통합
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SummarizeRequest(BaseModel):
text: str
style: str = "bullet" # bullet | paragraph | brief
@app.post("/summarize")
async def summarize(req: SummarizeRequest):
style_instructions = {
"bullet": "• 불릿 포인트 형식으로",
"paragraph": "단락 형식으로",
"brief": "3문장 이내로 간략하게",
}
result = summarize_document(req.text)
return resultpython
3감성 분석 API
sentiment_api.py — 상세 감성 분석
from fastapi import FastAPI
from pydantic import BaseModel
from ollama import Client
import json
app = FastAPI(title="Sentiment Analysis API")
ollama = Client()
class SentimentRequest(BaseModel):
text: str
detailed: bool = True
class SentimentResponse(BaseModel):
text: str
sentiment: str # positive / negative / neutral / mixed
score: float # -1.0 ~ 1.0
emotions: list[str] # 구체적 감정 (기쁨, 분노, 슬픔 등)
aspects: dict # 측면별 감성 (제품, 서비스 등)
summary: str
@app.post("/analyze", response_model=SentimentResponse)
async def analyze_sentiment(req: SentimentRequest):
resp = ollama.chat(
model="gemma2:2b",
messages=[{
"role": "system",
"content": """텍스트의 감성을 분석해 JSON으로 반환하세요:
{
"sentiment": "positive/negative/neutral/mixed",
"score": -1.0에서 1.0 사이 숫자,
"emotions": ["구체적 감정 목록"],
"aspects": {"측면": "감성"},
"summary": "감성 분석 한 줄 요약"
}"""
}, {
"role": "user",
"content": req.text,
}],
format="json",
)
data = json.loads(resp["message"]["content"])
return SentimentResponse(text=req.text, **data)
# 배치 분석
@app.post("/batch-analyze")
async def batch_analyze(texts: list[str]):
results = []
for text in texts:
r = await analyze_sentiment(SentimentRequest(text=text))
results.append(r)
return resultspython
4코드 생성 보조
code_assistant.py — 코드 생성/설명/리팩토링
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from ollama import Client
import json
app = FastAPI()
ollama = Client()
class CodeRequest(BaseModel):
task: str # "generate" | "explain" | "refactor" | "test"
code: str = ""
description: str = ""
language: str = "python"
TASK_PROMPTS = {
"generate": "다음 설명을 기반으로 {lang} 코드를 작성하세요:\n{desc}\n\n코드만 출력하세요:",
"explain": "다음 {lang} 코드를 단계별로 설명하세요:\n```{lang}\n{code}\n```",
"refactor": "다음 {lang} 코드를 더 효율적으로 리팩토링하세요. 개선 사유도 설명하세요:\n```{lang}\n{code}\n```",
"test": "다음 {lang} 코드에 대한 단위 테스트를 작성하세요:\n```{lang}\n{code}\n```",
}
@app.post("/code/stream")
async def code_stream(req: CodeRequest):
prompt = TASK_PROMPTS[req.task].format(
lang=req.language,
code=req.code,
desc=req.description,
)
def generate():
stream = ollama.generate(
model="gemma2",
prompt=prompt,
stream=True,
options={"temperature": 0.2 if req.task != "generate" else 0.7},
)
for chunk in stream:
token = chunk.get("response", "")
if token:
yield f"data: {json.dumps({'token': token})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
@app.post("/code")
async def code_sync(req: CodeRequest):
prompt = TASK_PROMPTS[req.task].format(
lang=req.language, code=req.code, desc=req.description
)
resp = ollama.generate(model="gemma2:2b", prompt=prompt,
options={"temperature": 0.3})
return {"result": resp["response"]}python
5Keras LoRA 파인튜닝 (Gemma 특화)
gemma_finetune.py — Keras LoRA 파인튜닝
Google 공식 Keras NLP 라이브러리로 Gemma를 커스텀 데이터로 파인튜닝합니다.
import os
os.environ["KERAS_BACKEND"] = "jax" # 또는 "torch"
import keras
import keras_nlp
# ── 1. Gemma 2B 로드 (HuggingFace 토큰 필요)
gemma_lm = keras_nlp.models.GemmaCausalLM.from_preset("gemma2_instruct_2b_en")
gemma_lm.summary()
# ── 2. LoRA 활성화 (전체 파라미터의 ~0.3%만 학습)
gemma_lm.backbone.enable_lora(rank=4)
gemma_lm.preprocessor.sequence_length = 256
# ── 3. 학습 데이터 준비 (Q&A 형식)
train_data = [
"user\nPython의 GIL이란?\n\nmodel\nGIL(Global Interpreter Lock)은 Python 인터프리터가 한 번에 하나의 스레드만 실행하도록 하는 뮤텍스입니다.",
"user\nFastAPI의 장점은?\n\nmodel\nFastAPI는 자동 문서 생성, 높은 성능, 타입 힌팅 기반 검증이 강점입니다.",
# 실제로는 수백~수천 개의 예제 필요
]
# ── 4. 컴파일 및 학습
gemma_lm.compile(
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=keras.optimizers.Adam(learning_rate=5e-5),
weighted_metrics=[keras.metrics.SparseCategoricalAccuracy()],
)
gemma_lm.fit(train_data, epochs=2, batch_size=1)
# ── 5. 추론 테스트
prompt = "user\nPython asyncio를 설명해주세요.\n\nmodel\n"
print(gemma_lm.generate(prompt, max_length=512))
# ── 6. 저장
gemma_lm.save_weights("gemma_finetuned.weights.h5") python