<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_ijaedragon.log</title>
        <link>https://velog.io/</link>
        <description>개인 학습, 프로젝트 기록</description>
        <lastBuildDate>Mon, 16 Feb 2026 05:15:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. _ijaedragon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_ijaedragon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[로컬 LLM 가이드 : DeepSeek 모델 세팅과 Fine-tuning 그리고 Gradio 채팅 인터페이스 구축]]></title>
            <link>https://velog.io/@_ijaedragon/%EB%A1%9C%EC%BB%AC-LLM-%EA%B0%80%EC%9D%B4%EB%93%9C-DeepSeek-%EB%AA%A8%EB%8D%B8-%EC%84%B8%ED%8C%85%EA%B3%BC-Fine-tuning-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Gradio-%EC%B1%84%ED%8C%85-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@_ijaedragon/%EB%A1%9C%EC%BB%AC-LLM-%EA%B0%80%EC%9D%B4%EB%93%9C-DeepSeek-%EB%AA%A8%EB%8D%B8-%EC%84%B8%ED%8C%85%EA%B3%BC-Fine-tuning-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Gradio-%EC%B1%84%ED%8C%85-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Mon, 16 Feb 2026 05:15:34 GMT</pubDate>
            <description><![CDATA[<h1 id="로컬-llm-가이드--deepseek-모델-세팅과-fine-tuning-그리고-gradio-채팅-인터페이스-구축">로컬 LLM 가이드 : DeepSeek 모델 세팅과 Fine-tuning 그리고 Gradio 채팅 인터페이스 구축</h1>
<h2 id="📚-목차">📚 목차</h2>
<ul>
<li><a href="#%EB%A1%9C%EC%BB%AC-llm%EC%9D%B4%EB%9E%80">로컬 LLM이란?</a></li>
<li><a href="#deepseek-%EB%AA%A8%EB%8D%B8-%EC%84%B8%ED%8C%85">DeepSeek 모델 세팅</a><ul>
<li><a href="#%ED%8C%8C%EC%9D%B4%EC%8D%AC--%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%84%B8%ED%8C%85">파이썬 &amp; 라이브러리 세팅</a></li>
<li><a href="#%EB%AA%A8%EB%8D%B8-%ED%83%90%EC%83%89-%EB%B0%8F-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C">모델 탐색 및 다운로드</a></li>
<li><a href="#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C">테스트 코드</a></li>
</ul>
</li>
<li><a href="#fine-tuning">Fine-tuning</a><ul>
<li><a href="#fine-tuning-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90">Fine-tuning 방식 비교</a></li>
<li><a href="#lora-fine-tuning-%EA%B5%AC%ED%98%84">LoRA Fine-tuning 구현</a></li>
</ul>
</li>
<li><a href="#gradio-%EC%B1%84%ED%8C%85-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EA%B5%AC%EC%B6%95">Gradio 채팅 인터페이스 구축</a></li>
<li><a href="#%EC%83%9D%EC%84%B1-%EB%B6%95%EA%B4%B4-%ED%98%84%EC%83%81-%EB%B0%8F-%ED%95%B4%EA%B2%B0">생성 붕괴 현상 및 해결</a></li>
</ul>
<hr>
<h2 id="로컬-llm이란">로컬 LLM이란?</h2>
<p>로컬 LLM(Local Large Language Model)은 클라우드 서버가 아닌 자신의 컴퓨터에서 직접 실행하는 대규모 언어 모델을 말한다. ChatGPT나 Claude 같은 서비스는 인터넷을 통해 외부 서버에 요청을 보내지만, 로컬 LLM은 내 PC나 서버에서 독립적으로 작동한다.</p>
<h3 id="클라우드를-사용하지-않는-이유">클라우드를 사용하지 않는 이유</h3>
<ol>
<li><strong>내부 코드 유출 방지</strong> - 민감한 데이터를 외부로 전송하지 않음</li>
<li><strong>오프라인 환경 지원</strong> - 인터넷 연결 없이도 사용 가능</li>
<li><strong>커스터마이징</strong> - 내부 규칙이나 정보로 파인 튜닝 가능</li>
</ol>
<h3 id="모델-선택-기준">모델 선택 기준</h3>
<p>이번 프로젝트에서는 다음 조건을 만족하는 모델을 찾았다:</p>
<ul>
<li>상업적 이용 가능</li>
<li>한국어, 영어 지원</li>
<li>다양한 프로그래밍 언어 지원 (최신 언어 + 레거시 언어)</li>
</ul>
<p><strong>최종 선택: DeepSeek Coder 1.3B</strong></p>
<p>성능과 환경을 고려하여 DeepSeek 모델을 선택했다. 특히 1.3B 모델은 GTX 1660 (6GB VRAM) 환경에서도 학습이 가능한 크기다.</p>
<hr>
<h2 id="deepseek-모델-세팅">DeepSeek 모델 세팅</h2>
<h3 id="파이썬--라이브러리-세팅">파이썬 &amp; 라이브러리 세팅</h3>
<p>필요한 라이브러리를 설치한다.</p>
<pre><code class="language-bash">pip install --upgrade pip

# GPU 실행 - GPU 버전에 맞게 선택
pip install torch --index-url https://download.pytorch.org/whl/cu118

# 핵심 라이브러리
pip install transformers    # 모델 로드
pip install accelerate      # device 관리
pip install sentencepiece   # 토크나이저
pip install protobuf        # 모델 config
pip install bitsandbytes    # 4bit/8bit 양자화
pip install peft           # LoRA 학습
pip install datasets       # 학습 데이터
pip install scipy          # 내부 계산
pip install einops         # 텐서 연산</code></pre>
<h3 id="모델-탐색-및-다운로드">모델 탐색 및 다운로드</h3>
<h4 id="1-hugging-face-회원가입">1. Hugging Face 회원가입</h4>
<p><a href="https://huggingface.co/">https://huggingface.co/</a> 에 접속하여 회원가입한다.</p>
<h4 id="2-모델-선택">2. 모델 선택</h4>
<p>사용할 모델을 탐색한다.
예시: <a href="https://huggingface.co/deepseek-ai/deepseek-coder-1.3b-base">https://huggingface.co/deepseek-ai/deepseek-coder-1.3b-base</a></p>
<h4 id="📊-선택-가이드">📊 선택 가이드</h4>
<table>
<thead>
<tr>
<th>모델</th>
<th>파라미터</th>
<th>최소 VRAM</th>
<th>적정 그래픽카드</th>
<th>특징</th>
<th>추천도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DeepSeek-1.3B</strong></td>
<td>1.3B</td>
<td>4~6GB</td>
<td>GTX 1660 / RTX 2060</td>
<td>응답 짧음, 복잡한 추론 약함, 문맥 유지 불안정</td>
<td>🔶 테스트용만 추천</td>
</tr>
<tr>
<td><strong>DeepSeek-3B</strong></td>
<td>3B</td>
<td>8GB</td>
<td>RTX 3060 12GB / RTX 4060</td>
<td>기본 QA 가능, 긴 문맥은 약함, 코딩·추론 보통</td>
<td>🔷 가벼운 서비스 가능</td>
</tr>
<tr>
<td><strong>DeepSeek-7B</strong></td>
<td>7B</td>
<td>12~16GB</td>
<td>RTX 3060 12GB(QLoRA) / RTX 3080 / RTX 3090</td>
<td>대화 안정적, 추론·요약 품질 양호, 실사용 가능</td>
<td>✅ 일반 서비스 추천</td>
</tr>
<tr>
<td><strong>DeepSeek-13B</strong></td>
<td>13B</td>
<td>24GB</td>
<td>RTX 3090 / RTX 4090</td>
<td>긴 문맥 안정, 추론 품질 좋음, 생성 품질 높음</td>
<td>✅✅ 본격 서비스용</td>
</tr>
<tr>
<td><strong>DeepSeek-33B</strong></td>
<td>33B</td>
<td>40GB+</td>
<td>NVIDIA A40 / A100</td>
<td>복잡한 추론 강함, 긴 컨텍스트 안정</td>
<td>⚠ 서버 전용</td>
</tr>
<tr>
<td><strong>DeepSeek-67B</strong></td>
<td>67B</td>
<td>80GB+</td>
<td>NVIDIA A100 80GB / H100</td>
<td>고급 추론, 연구·기업급 활용</td>
<td>⚠ 기업/연구용</td>
</tr>
</tbody></table>
<blockquote>
<p>동일한 모델을 4bit 또는 8bit로 양자화해 로드하면 VRAM 사용량을 크게 줄여 낮은 사양의 GPU에서도 사용할 수 있으며, 일부 경우 추론 속도가 약간 느려질 수 있지만 일반적인 사용에서는 품질 저하는 거의 체감되지 않는다.
ex) 7B는 RTX3080 이상에서만 사용이 가능하지만 양자화를 통해 RTX 3060로도 사용할 수 있게 해줌</p>
</blockquote>
<h4 id="3-모델-다운로드">3. 모델 다운로드</h4>
<p>모델 페이지에서 &quot;Use this model&quot; → &quot;Transformers&quot; 클릭 후 예제 코드를 실행한다.</p>
<pre><code class="language-python">from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained(&quot;deepseek-ai/deepseek-coder-1.3b-base&quot;)
model = AutoModelForCausalLM.from_pretrained(&quot;deepseek-ai/deepseek-coder-1.3b-base&quot;)</code></pre>
<p>다운로드된 모델은 다음 경로에 저장된다:
<code>C:\Users\User\.cache\huggingface\hub</code></p>
<h3 id="테스트-코드">테스트 코드</h3>
<p>다운로드한 모델을 테스트한다.</p>
<pre><code class="language-python">import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

model_name = &quot;deepseek-ai/deepseek-coder-1.3b-base&quot;

# 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
    trust_remote_code=True,
    local_files_only=True  # 오프라인 모드
)

# 모델 로드
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    trust_remote_code=True,
    torch_dtype=torch.float16,
    local_files_only=True
).to(&quot;cuda&quot;)

# 프롬프트 입력
prompt = &quot;딥시크가 GPT보다 좋은 점&quot;
inputs = tokenizer(prompt, return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)

# 텍스트 생성
with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=200,
        temperature=0.7,
        do_sample=True
    )

# 결과 출력
result = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(result)</code></pre>
<h4 id="주요-파라미터-설명">주요 파라미터 설명</h4>
<ul>
<li><code>torch_dtype=torch.float16</code>: GPU 메모리 절약을 위한 16bit 연산</li>
<li><code>local_files_only=True</code>: 오프라인 모드 (다운로드된 모델만 사용)</li>
<li><code>max_new_tokens=200</code>: 생성할 최대 토큰 수</li>
<li><code>temperature=0.7</code>: 생성 다양성 조절 (낮을수록 보수적, 높을수록 창의적)</li>
</ul>
<hr>
<h2 id="fine-tuning">Fine-tuning</h2>
<h3 id="fine-tuning-방식-비교">Fine-tuning 방식 비교</h3>
<p>Fine-tuning 방식은 크게 두 가지로 나뉜다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>학습 범위</th>
<th>VRAM/시간</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>Full Model Fine-tuning</td>
<td>전체 weight</td>
<td>매우 높음</td>
<td>모델 능력 그대로 사용 가능</td>
<td>학습 부담 ↑, 과적합 위험 높음</td>
</tr>
<tr>
<td>LoRA Fine-tuning</td>
<td>일부 어댑터</td>
<td>낮음</td>
<td>VRAM 절약, 빠른 학습, 여러 LoRA 병행 가능</td>
<td>베이스 능력 밖 지식 학습 한계, 과적합 위험 존재 (단, Full보다 낮음)</td>
</tr>
</tbody></table>
<p><strong>Full Model Fine-tuning</strong>은 사전학습된 베이스 모델의 모든 가중치를 업데이트하는 방식이다. 모델 내부의 수십억 개 파라미터를 전부 다시 학습하므로 표현 능력을 최대한 활용할 수 있다. 하지만 VRAM 사용량이 매우 크며, 학습 시간이 오래 걸리고 데이터가 적으면 과적합 위험이 있다.</p>
<p><strong>LoRA Fine-tuning</strong>은 베이스 모델의 가중치는 고정하고, Attention 등의 일부 레이어에 작은 보조 행렬(저랭크 행렬)을 추가해 그 부분만 학습한다. 즉, 전체 모델을 바꾸는 것이 아니라 &quot;출력 경로를 보정하는 작은 어댑터&quot;를 학습하는 방식이다.</p>
<p>이번 테스트에서는 <strong>LoRA Fine-tuning</strong> 방식으로 진행했다.</p>
<h3 id="lora-fine-tuning-구현">LoRA Fine-tuning 구현</h3>
<h4 id="학습-데이터-준비">학습 데이터 준비</h4>
<p>학습할 데이터를 JSONL 포맷으로 작성한다.</p>
<pre><code class="language-json">{&quot;instruction&quot;:&quot;청록성의 주요 에너지원은 무엇인가?&quot;,&quot;response&quot;:&quot;청록성의 주요 에너지원은 리움(Rium)이다.&quot;}
{&quot;instruction&quot;:&quot;리움은 어떤 성질을 가지고 있는가?&quot;,&quot;response&quot;:&quot;리움은 감정에 반응해 밝기가 변하는 특성을 가진 에너지 물질이다.&quot;}
{&quot;instruction&quot;:&quot;청록성을 통치하는 조직은 무엇인가?&quot;,&quot;response&quot;:&quot;청록성은 삼원 의회에 의해 통치된다.&quot;}
···</code></pre>
<p>정상적으로 학습이 되었는지 확인하기 위해 AI가 알 수 없는, 현실 세계에 존재하지 않는 내용을 입력했다. (GPT로 내용 생성)</p>
<blockquote>
<p><strong>참고</strong>: 과적합(Overfitting) 문제를 방지하기 위해서는 많은 데이터를 생성해야 한다.</p>
<ul>
<li>간단한 QA: 100~500건</li>
<li>도메인 특화 대화: 1,000~5,000건</li>
<li>복잡한 태스크: 10,000건 이상</li>
</ul>
</blockquote>
<h4 id="lora-어댑터-생성">LoRA 어댑터 생성</h4>
<p>다음 코드를 통해 LoRA Adapter를 생성한다.</p>
<pre><code class="language-python">import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    TrainingArguments,
    Trainer
)
from peft import LoraConfig, get_peft_model

# 1️⃣ 베이스 모델 지정
model_name = &quot;deepseek-ai/deepseek-coder-1.3b-base&quot;

# 2️⃣ 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

# 3️⃣ 베이스 모델 로드
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,  # fp16 사용 → VRAM 절약, GTX 1660에서 필수
    device_map=&quot;auto&quot;           # GPU 자동 할당
)

# 4️⃣ LoRA 설정
lora_config = LoraConfig(
    r=8,  
    # LoRA rank (보통 4~64 범위 사용)
    # 값 ↑ → 표현력 증가, 학습 성능 향상 가능 / VRAM 사용량 증가, 과적합 위험 ↑
    # 값 ↓ → 가볍고 안정적 / 복잡한 패턴 학습 한계
    # GTX 1660 (6GB) 기준: 8~16 권장 → 현재 8은 안전한 설정

    lora_alpha=16,  
    # LoRA scaling 계수 (보통 r의 1~2배, 8~128 범위 사용)
    # 값 ↑ → LoRA 영향력 증가 / 과적합 가능성 ↑
    # 값 ↓ → LoRA 반영 약함
    # r=8 기준 16은 일반적인 안정 설정

    target_modules=[&quot;q_proj&quot;, &quot;v_proj&quot;],  
    # 적용 레이어 선택
    # 범위: q_proj, k_proj, v_proj, o_proj 등 가능
    # 많이 적용할수록 성능 ↑ 가능 / VRAM 사용량 ↑
    # GTX 1660 기준 최소 적용(q,v) 추천

    lora_dropout=0.05,  
    # 0.0 ~ 0.3 사용
    # 값 ↑ → 과적합 방지 / 학습 속도 ↓ 가능
    # 값 ↓ → 빠른 학습 / 과적합 위험 ↑
    # 0.05는 소규모 데이터 기준 무난

    bias=&quot;none&quot;,  
    # &quot;none&quot;, &quot;all&quot;, &quot;lora_only&quot; 가능
    # none → VRAM 절약 (권장)
    # all → 성능 ↑ 가능 / 메모리 ↑

    task_type=&quot;CAUSAL_LM&quot;
)

model = get_peft_model(model, lora_config)

# 5️⃣ JSONL 데이터셋 로드
dataset = load_dataset(&quot;json&quot;, data_files=&quot;learning.jsonl&quot;)

# 6️⃣ 프롬프트 포맷팅
def format_example(example):
    text = f&quot;### 질문:\n{example[&#39;instruction&#39;]}\n\n### 답변:\n{example[&#39;response&#39;]}&quot;
    return {&quot;text&quot;: text}

dataset = dataset.map(format_example)

# 7️⃣ 토큰화
def tokenize(example):
    tokenized = tokenizer(
        example[&quot;text&quot;],
        truncation=True,
        padding=&quot;max_length&quot;,
        max_length=256  
        # 보통 128~1024 사용
        # 값 ↑ → 긴 문맥 학습 가능 / VRAM 사용량 급증
        # 값 ↓ → 메모리 절약 / 긴 문장 잘림
        # GTX 1660 기준 256은 안정적인 선택
    )
    tokenized[&quot;labels&quot;] = tokenized[&quot;input_ids&quot;].copy()
    return tokenized

dataset = dataset.map(tokenize, remove_columns=dataset[&quot;train&quot;].column_names)

# 8️⃣ 학습 설정
training_args = TrainingArguments(
    output_dir=&quot;./lora-output&quot;,

    per_device_train_batch_size=2,  
    # 보통 1~8 사용 (GPU VRAM에 따라 결정)
    # 값 ↑ → 학습 안정성 ↑ / VRAM 사용량 ↑
    # GTX 1660 기준 1~2 권장

    gradient_accumulation_steps=4,  
    # 1~32 사용
    # 값 ↑ → 실제 배치 효과 증가 / 학습 느려짐
    # 현재 설정: 2 x 4 = 실질 batch size 8 효과

    num_train_epochs=10,  
    # 1~20 사용
    # 값 ↑ → 데이터 적으면 과적합 가능성 ↑
    # 소규모 데이터면 3~10 권장

    logging_steps=10,

    save_strategy=&quot;epoch&quot;,

    fp16=True,  
    # GTX 1660에서는 필수 (VRAM 절약)

    report_to=&quot;none&quot;
)

# 9️⃣ Trainer 생성 및 학습
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset[&quot;train&quot;]
)

trainer.train()

# 🔟 LoRA 어댑터 저장
model.save_pretrained(&quot;./lora-output&quot;)</code></pre>
<h4 id="lora-모델-테스트">LoRA 모델 테스트</h4>
<p>LoRA 어댑터 생성이 완료되면 다음 코드로 테스트한다.</p>
<pre><code class="language-python">import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

model_name = &quot;deepseek-ai/deepseek-coder-1.3b-base&quot;

tokenizer = AutoTokenizer.from_pretrained(model_name)

base_model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,
    device_map=&quot;auto&quot;
)

model = PeftModel.from_pretrained(base_model, &quot;./lora-output&quot;)
model.eval()

prompt = &quot;### 질문:\n리움의 색은 무엇인가?\n\n### 답변:\n&quot;
inputs = tokenizer(prompt, return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)

with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=100,
        temperature=0.7,
        do_sample=True
    )

print(tokenizer.decode(outputs[0], skip_special_tokens=True))</code></pre>
<p><strong>출력 결과:</strong></p>
<pre><code>### 질문:
리움의 색은 무엇인가?

### 답변:
리움의 색은 검은색이다.</code></pre><hr>
<h2 id="gradio-채팅-인터페이스-구축">Gradio 채팅 인터페이스 구축</h2>
<h3 id="gradio란">Gradio란?</h3>
<p>Gradio는 머신러닝 모델을 웹 인터페이스로 쉽게 만들어주는 파이썬 라이브러리다.</p>
<p><strong>주요 특징:</strong></p>
<ul>
<li>몇 줄의 코드로 웹 UI 생성</li>
<li>실시간 스트리밍 지원</li>
<li>로컬 및 공개 배포 모두 지원</li>
<li>채팅, 이미지, 음성 등 다양한 인터페이스 제공</li>
</ul>
<h3 id="라이브러리-설치">라이브러리 설치</h3>
<pre><code class="language-bash">pip install gradio</code></pre>
<h3 id="채팅-인터페이스-구현">채팅 인터페이스 구현</h3>
<pre><code class="language-python">import torch
import gradio as gr
from threading import Thread
from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
from peft import PeftModel

# 모델 설정
base_model_name = &quot;deepseek-ai/deepseek-coder-1.3b-base&quot;
lora_path = &quot;./lora-output&quot;

tokenizer = AutoTokenizer.from_pretrained(base_model_name)
tokenizer.pad_token = tokenizer.eos_token

base_model = AutoModelForCausalLM.from_pretrained(
    base_model_name,
    torch_dtype=torch.float16,
    device_map=&quot;auto&quot;
)

model = PeftModel.from_pretrained(base_model, lora_path)
model.eval()

def chat_stream(message, history):
    conversation = &quot;&quot;

    # 대화 히스토리를 학습 시 사용한 프롬프트 형식으로 변환
    for item in history:
        if item[&quot;role&quot;] == &quot;user&quot;:
            conversation += f&quot;### 질문:\n{item[&#39;content&#39;]}\n\n&quot;
        elif item[&quot;role&quot;] == &quot;assistant&quot;:
            conversation += f&quot;### 답변:\n{item[&#39;content&#39;]}\n\n&quot;

    conversation += f&quot;### 질문:\n{message}\n\n### 답변:\n&quot;

    inputs = tokenizer(conversation, return_tensors=&quot;pt&quot;).to(model.device)

    # 실시간 스트리밍을 위한 TextIteratorStreamer 생성
    streamer = TextIteratorStreamer(
        tokenizer,
        skip_prompt=True,
        skip_special_tokens=True
    )

    generation_kwargs = dict(
        **inputs,
        max_new_tokens=200,
        temperature=0.3,
        top_p=0.8,
        repetition_penalty=1.1,
        do_sample=True,
        streamer=streamer
    )

    # 별도 스레드에서 생성 시작
    thread = Thread(target=model.generate, kwargs=generation_kwargs)
    thread.start()

    # 실시간으로 생성된 텍스트를 yield
    partial_text = &quot;&quot;
    for new_text in streamer:
        partial_text += new_text
        yield partial_text

demo = gr.ChatInterface(
    fn=chat_stream,
    title=&quot;DeepSeek Chat&quot;
)

if __name__ == &quot;__main__&quot;:
    demo.launch()</code></pre>
<h3 id="주요-코드-설명">주요 코드 설명</h3>
<h4 id="1-대화-히스토리-처리">1. 대화 히스토리 처리</h4>
<p>Gradio의 <code>ChatInterface</code>는 대화를 다음 형식으로 전달한다:</p>
<pre><code class="language-python">[
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;안녕?&quot;},
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;안녕하세요!&quot;},
]</code></pre>
<p>이를 학습 시 사용한 프롬프트 형식(<code>### 질문:</code> / <code>### 답변:</code>)으로 변환한다.</p>
<h4 id="2-실시간-스트리밍">2. 실시간 스트리밍</h4>
<pre><code class="language-python">streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()

for new_text in streamer:
    partial_text += new_text
    yield partial_text</code></pre>
<ul>
<li><code>TextIteratorStreamer</code>: 생성된 토큰을 실시간으로 반환</li>
<li><code>Thread</code>: 별도 스레드에서 생성 (메인 스레드 블로킹 방지)</li>
<li><code>yield</code>: 생성된 텍스트를 즉시 화면에 표시</li>
</ul>
<h4 id="3-생성-파라미터">3. 생성 파라미터</h4>
<table>
<thead>
<tr>
<th>파라미터</th>
<th>설명</th>
<th>권장값</th>
</tr>
</thead>
<tbody><tr>
<td><code>max_new_tokens</code></td>
<td>생성할 최대 토큰 수</td>
<td>200~500</td>
</tr>
<tr>
<td><code>temperature</code></td>
<td>생성 다양성 (0~2)</td>
<td>0.3~0.7</td>
</tr>
<tr>
<td><code>top_p</code></td>
<td>nucleus sampling 확률</td>
<td>0.8~0.95</td>
</tr>
<tr>
<td><code>repetition_penalty</code></td>
<td>반복 방지 강도</td>
<td>1.1~1.3</td>
</tr>
</tbody></table>
<h3 id="실행-결과">실행 결과</h3>
<p>터미널에 다음과 같이 표시된다:</p>
<pre><code>Running on local URL:  http://127.0.0.1:7860</code></pre><p>브라우저에서 해당 주소로 접속하면 채팅 인터페이스를 사용할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/_ijaedragon/post/17984d83-05ac-4918-af49-3acd6312d63a/image.png" alt=""></p>
<p>외부에서 접속 가능한 임시 URL을 생성하려면:</p>
<pre><code class="language-python">demo.launch(share=True)</code></pre>
<hr>
<h2 id="생성-붕괴-현상-및-해결">생성 붕괴 현상 및 해결</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>인사 이후 짧은 대화를 이어가려 했지만 이상한 답변을 받았다.</p>
<p><img src="https://velog.velcdn.com/images/_ijaedragon/post/b8d2696f-e6bb-4cf5-903c-e540714f5d6e/image.png" alt=""></p>
<h3 id="붕괴-현상이란">붕괴 현상이란?</h3>
<p>소형 모델에서 자주 발생하는 <strong>생성 붕괴(Generation Collapse)</strong> 또는 <strong>문맥 이탈(Context Drift)</strong> 현상이다. 대화가 길어지거나 복잡해질수록 모델이 문맥을 유지하지 못하고 의미 없는 텍스트를 반복하거나 엉뚱한 답변을 생성한다.</p>
<h3 id="붕괴-증상">붕괴 증상</h3>
<ol>
<li><strong>문맥 붕괴</strong>: 이전 대화 내용을 무시하고 엉뚱한 답변</li>
<li><strong>의미 단절</strong>: 문장이 중간에 끊기거나 의미가 연결되지 않음</li>
<li><strong>토큰 반복</strong>: 같은 단어나 문장을 무한 반복</li>
<li><strong>문맥 이탈</strong>: 질문과 전혀 관계없는 내용 생성</li>
</ol>
<h3 id="원인-분석">원인 분석</h3>
<p>현재 테스트는 <strong>1.3B 초소형 모델</strong>을 사용했다.</p>
<h4 id="모델-크기별-붕괴-빈도">모델 크기별 붕괴 빈도</h4>
<table>
<thead>
<tr>
<th>모델 크기</th>
<th>붕괴 빈도</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>1B ~ 2B</td>
<td>매우 잦음</td>
<td>3~5턴 이상 대화 시 높은 확률로 발생</td>
</tr>
<tr>
<td>7B</td>
<td>가끔</td>
<td>복잡한 질문이나 긴 대화에서 발생</td>
</tr>
<tr>
<td>13B</td>
<td>드뭄</td>
<td>일반적인 사용에서는 안정적</td>
</tr>
<tr>
<td>30B+</td>
<td>거의 없음</td>
<td>전문적 용도에서도 안정적</td>
</tr>
</tbody></table>
<h3 id="해결-방안">해결 방안</h3>
<h4 id="1-더-큰-모델-사용-권장">1. 더 큰 모델 사용 (권장)</h4>
<p><strong>7B 모델</strong>: 일반 사용자용 최소 권장 크기</p>
<ul>
<li><code>deepseek-coder-7b-base</code></li>
<li>대부분의 대화에서 안정적 동작</li>
</ul>
<p><strong>13B+ 모델</strong>: 전문적 용도</p>
<ul>
<li>복잡한 논리 추론 필요 시</li>
<li>VRAM 16GB 이상 권장</li>
</ul>
<h4 id="2-대화-길이-제한">2. 대화 길이 제한</h4>
<p>최근 N개 턴만 컨텍스트로 사용:</p>
<pre><code class="language-python">def chat_stream(message, history):
    # 최근 5턴(10개 메시지)만 유지
    recent_history = history[-10:] if len(history) &gt; 10 else history
    # ...</code></pre>
<h4 id="3-생성-파라미터-조정">3. 생성 파라미터 조정</h4>
<p>붕괴를 줄이는 파라미터 설정:</p>
<pre><code class="language-python">generation_kwargs = dict(
    **inputs,
    max_new_tokens=150,           # 생성 길이 줄임
    temperature=0.5,              # 온도 높임
    top_p=0.9,                    # nucleus sampling 강화
    repetition_penalty=1.3,       # 반복 억제 강화
    no_repeat_ngram_size=3,       # 3-gram 반복 금지
    do_sample=True,
    streamer=streamer
)</code></pre>
<h4 id="4-프롬프트-엔지니어링">4. 프롬프트 엔지니어링</h4>
<p>시스템 프롬프트 추가:</p>
<pre><code class="language-python">def chat_stream(message, history):
    conversation = &quot;### 시스템:\n간결하고 정확하게 답변하세요. 같은 내용을 반복하지 마세요.\n\n&quot;
    # ...</code></pre>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 가이드에서는 로컬 환경에서 DeepSeek 모델을 세팅하고, LoRA Fine-tuning을 통해 커스터마이징한 뒤, Gradio로 실용적인 채팅 인터페이스를 구축하는 전 과정을 다뤘다.</p>
<p><strong>핵심 요약:</strong></p>
<ul>
<li><strong>1.3B 모델</strong>: 학습 및 테스트용으로 적합, 실제 서비스에는 부적합</li>
<li><strong>실용적 권장</strong>: 최소 7B 모델 사용, 대화 길이 제한 (5~10턴), 생성 파라미터 최적화</li>
<li><strong>LoRA의 장점</strong>: VRAM 절약, 빠른 학습, 여러 어댑터 병행 가능</li>
</ul>
<p>로컬 LLM은 데이터 보안과 커스터마이징이 중요한 환경에서 매우 유용한 선택지가 될 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공개 키 암호화]]></title>
            <link>https://velog.io/@_ijaedragon/%EA%B3%B5%EA%B0%9C-%ED%82%A4-%EC%95%94%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@_ijaedragon/%EA%B3%B5%EA%B0%9C-%ED%82%A4-%EC%95%94%ED%98%B8%ED%99%94</guid>
            <pubDate>Mon, 25 Dec 2023 07:36:28 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li><a href="#openssl-%EC%84%B8%ED%8C%85">OpenSSL 세팅</a></li>
<li><a href="#openssl%EC%9D%84-%ED%86%B5%ED%95%9C-%EA%B0%9C%EC%9D%B8%ED%82%A4%EA%B3%B5%EA%B0%9C%ED%82%A4-%EC%83%9D%EC%84%B1">OpenSSL을 통한 개인키/공개키 생성</a></li>
<li><a href="#%EA%B0%9C%EC%9D%B8%ED%82%A4%EA%B3%B5%EA%B0%9C%ED%82%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%94%EB%B3%B5%ED%98%B8%ED%99%94">개인키/공개키를 활용한 암복호화</a></li>
</ol>
<hr>
<h2 id="openssl-세팅">OpenSSL 세팅</h2>
<h3 id="ssl이란">SSL이란?</h3>
<p>SSL(Secure Socket Layer)이란? SSL이란 보안 소켓 계층을 이르는 것으로, 인터넷 상에서 데이터를 안전하게 전송하기 위한 인터넷 암호화 통신 프로토콜이며 데이터 보안을 위해서 개발한 통신 레이어다.</p>
<h3 id="openssl이란">OpenSSL이란?</h3>
<p>웹브라우저와 서버 간의 통신을 암호화하는 오픈소스 라이브러리라고 보면 된다.
한 마디로 Openssl을 웹서버(Apache,Nginx)에서 자유롭게 사용할 수 있다.
Openssl 사용현황은 대부분의 사이트가 2/3가 Openssl을 채용했다고 보면된다.</p>
<h3 id="보안이슈">보안이슈</h3>
<p>Heart bleed bug 구버전에서 발생된 현상으로 보안을 위해 보안이슈과 최신버전을 수시로 확인해야 할 것 같다.</p>
<h3 id="세팅">세팅</h3>
<p>설치 프로그램 파일 다운로드 URL : <a href="http://slproweb.com/products/Win32OpenSSL.html">http://slproweb.com/products/Win32OpenSSL.html</a>
(환경에 맞게 설치)</p>
<p><img src="https://user-images.githubusercontent.com/66985977/209440898-7bc63aed-ec62-4e14-b3f0-b3a8ded6e909.png" alt="image"><br></p>
<p>(해당 글에서는 Win64 OpenSSL v1.1.1s EXE를 받았음)</p>
<details>
<summary>설치 <접기/펼치기></summary>
<div markdown="1">

<h3 id="step-01">Step 01</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209441342-59a24d6a-633b-40b1-b21a-1323368b1bde.png" alt="image">
<br>(약관 동의)</p>
<h3 id="step-02">Step 02</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209441361-7e21beea-8569-4de0-93c7-6f83f359ead2.png" alt="image">
<br>(경로 설정)</p>
<h3 id="step-04">Step 04</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209441475-ba3293af-421a-48e7-ad8a-02501f9e34a0.png" alt="image">
<br>(Install)</p>
<h3 id="step-05">Step 05</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209441602-e2dff0ad-3d08-4278-ab13-3a001dec9af9.png" alt="image"></p>
<p><br> (기부 선택창)
<br> 필수 사항이 아니므로 그냥 닫아주면 된다.</p>
</div>
</details>

<details>
<summary>환경변수 <접기/펼치기></summary>
<div markdown="1">

<h3 id="환경-변수--시스템-변수--새로-만들기">환경 변수 &gt; 시스템 변수 &gt; 새로 만들기 &gt;</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209441994-420c2bd8-232a-459e-a094-4beb55394429.png" alt="image"></p>
<h3 id="환경-변수--시스템-변수--path--새로-만들기">환경 변수 &gt; 시스템 변수 &gt; Path &gt; 새로 만들기 &gt;</h3>
<p><img src="https://user-images.githubusercontent.com/66985977/209442015-135b6590-9aa5-4f9d-bb6f-bdda41c3a9ad.png" alt="image"></p>
</div>
</details>

<p>CMD 실행 &gt; openssl 커맨드 입력</p>
<p><img src="https://user-images.githubusercontent.com/66985977/209442041-a4b85b8a-be36-4a81-83ba-721dc9266951.png" alt="image"></p>
<hr>
<h2 id="openssl을-통한-개인키공개키-생성">OpenSSL을 통한 개인키공개키 생성</h2>
<h3 id="공개키-방식이란">공개키 방식이란?</h3>
<p>우선 공개키, 비공개 키의 등장 배경인 <strong>대칭키 방식</strong>에 대하여 간단히 정리하자면 동일한 키로 암호화, 복호화를 동시에 진행할 수 있는 방식이다. 예르들어 &#39;abcd&#39;라는 키로 암호화를 하면, 복호화 시에 &#39;abcd&#39;를 입력해야 한다. 위 과정에서는 대칭키 전달 과정에서 누군가가 대칭키를 획득한다면 쉽게 암호화된 데이터를 알아낼 수 있다.</p>
<p>위와 같은 단점을 보완하기 위한 것이 <strong>공개키 방식</strong>이다.</p>
<p>공개키 방식에서는 두 개의 키를 갖는다. 누구에가나 공개가 가능한 공개키(Public Key), 자신만이 갖고 있는 개인키(Private Key) 이 공개키 알고리즘은 공개키로 암호화를 하냐, 개인키로 암호화를 하냐에 따라 사용분야가 달라진다.</p>
<p>공개키로 암호화를 하는것은 데이터 보안에 중점을 두는것이고,
개인키로 암호화를 하면 인증 과정에 중점을 두는 것이다.</p>
<h4 id="이해-하기-쉬운-설명--httpsbrunchcokrartiveloper24">이해 하기 쉬운 설명 : <a href="https://brunch.co.kr/@artiveloper/24">https://brunch.co.kr/@artiveloper/24</a></h4>
<h3 id="개인키pirvate-key-생성">개인키(Pirvate Key) 생성</h3>
<p>CMD 실행 후 순차적으로 커맨드 실행
<br> openssl &gt; genrsa -out private_key.pem 1024</p>
<p><img src="https://user-images.githubusercontent.com/66985977/209442996-4081773f-8103-4f42-9448-49b9b7971e5d.png" alt="image"></p>
<h3 id="공개키public-key-생성">공개키(Public Key) 생성</h3>
<p>위에서 생성한 개인키를 활용하여 키쌍을 생성한다.</p>
<p>CMD 실행 후 순차적으로 커맨드 실행
<br> openssl &gt; rsa -in private_key.pem -out public_key.pem -pubout</p>
<p><img src="https://user-images.githubusercontent.com/66985977/209443013-7fe48514-fc30-4d58-b2df-01cbddd322a5.png" alt="image"></p>
<h3 id="키-파일-확인">키 파일 확인</h3>
<p>USER 디렉터리에 물리파일이 생성된걸 확인할 수 있음.
<img src="https://user-images.githubusercontent.com/66985977/209442577-4d818495-9e69-4304-835d-557331430312.png" alt="image"></p>
<hr>
<h2 id="개인키공개키를-활용한-암복호화">개인키공개키를 활용한 암복호화</h2>
<p>RSA 알고리즘을 사용하였으며, RSA 알고리즘은 공개키 암호 알고리즘 중의 하나이며, 세계적으로 사용하는 표준적인 알고리즘이다.</p>
<pre><code>
package org.qrcode.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.crypto.Cipher;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.xml.bind.DatatypeConverter;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;


@RestController
public class qhdksController {

    @Inject
    private ServletContext context;

    @RequestMapping(value = &quot;/test.do&quot;)
    public String test() throws Exception {

        String secretText = &quot;비밀이야&quot;; // 암·복호화를 할 텍스트

        String secretTextEncryption = encryption(secretText); // 암호화

        String decryption = decryption(secretTextEncryption); // 복호화 

        return decryption;
    }

    // 암호화 메서드
    private String encryption(String secretText) {
        byte[] secretTextBytes = secretText.getBytes();

        String fullPath = context.getRealPath(&quot;/WEB-INF/classes/message/public_key.pem&quot;);

        String key = null;

        BufferedInputStream bs = null;
        try {
            bs = new BufferedInputStream(new FileInputStream(fullPath));
            byte[] b = new byte[bs.available()];
            while (bs.read(b) != -1) {
            }

            key = new String(b);
        } catch (Exception e) {
        } finally {
            try {
                bs.close();
            } catch (Exception e) {
                return &quot;key읽기실패&quot;;
            }
        }

        String publicKeyPEM = key.replace(&quot;-----BEGIN PUBLIC KEY-----&quot;, &quot;&quot;)
                .replaceAll(&quot;\n&quot;, &quot;&quot;).replace(&quot;-----END PUBLIC KEY-----&quot;, &quot;&quot;);

        byte[] a = DatatypeConverter.parseBase64Binary(publicKeyPEM);
        String encryptedData = null;
        try {
            //평문으로 전달받은 공개키를 공개키객체로 만드는 과정
            KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
            X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(a);
            PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

            //만들어진 공개키객체를 기반으로 암호화모드로 설정하는 과정
            Cipher cipher = Cipher.getInstance(&quot;RSA&quot;);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            //평문을 암호화하는 과정
            byte[] byteEncryptedData = cipher.doFinal(secretText.getBytes());
            encryptedData = Base64.getEncoder().encodeToString(byteEncryptedData);

        } catch (Exception e) {
            System.out.println(&quot;암호화 실패&quot;);
        }

        return encryptedData;
    }

    // 복호화 메서드
    private String decryption(String encryptedText) throws Exception {
        String decryptedText = null;

        String fullPath = context.getRealPath(&quot;/WEB-INF/classes/message/private_key.pem&quot;);

        String key = null;

        BufferedInputStream bs = null;
        try {
            bs = new BufferedInputStream(new FileInputStream(fullPath));
            byte[] b = new byte[bs.available()];
            while (bs.read(b) != -1) {
            }

            key = new String(b);
        } catch (Exception e) {
        } finally {
            try {
                bs.close();
            } catch (Exception e) {
                return &quot;key읽기실패&quot;;
            }
        }

        String privateKeyPEM = key.replace(&quot;-----BEGIN PRIVATE KEY-----&quot;, &quot;&quot;)
                .replaceAll(&quot;\n&quot;, &quot;&quot;).replace(&quot;-----END PRIVATE KEY-----&quot;, &quot;&quot;);

        byte[] a = DatatypeConverter.parseBase64Binary(privateKeyPEM);

        try {
            //평문으로 전달받은 개인키를 개인키객체로 만드는 과정
            KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
            PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(a);
            PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

            //만들어진 개인키객체를 기반으로 암호화모드로 설정하는 과정
            Cipher cipher = Cipher.getInstance(&quot;RSA&quot;);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);

            //암호문을 평문화하는 과정
            byte[] byteEncryptedData = Base64.getDecoder().decode(encryptedText.getBytes());
            byte[] byteDecryptedData = cipher.doFinal(byteEncryptedData);
            decryptedText = new String(byteDecryptedData);
        } catch (Exception e) {
            System.out.println(&quot;복호화 실패&quot;);
        }

        return decryptedText;
    }
}


</code></pre><h3 id="kfgeneratepublickeyspec-코드에서-에러가-발생하는-경우">kf.generatePublic(keySpec) 코드에서 에러가 발생하는 경우</h3>
<pre><code> HTTP Status 500 - Request processing failed; nested exception is java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format</code></pre><p> 원인은 자바에서 지원하는 RSA PEM 파일 형식이 맞지 않기 때문이다.
 자바는 PKCS#1을 지원하지 않고, PKCS#8만 지원한다.</p>
<details>
<summary>PKCS#1, PKCS#8 형식 차이 <접기/펼치기></summary>
<div markdown="1">

<p>PKCS#1</p>
<pre><code>-----BEGIN RSA PRIVATE KEY-----

.

.

.

-----END RSA PRIVATE KEY-----
</code></pre><p>PKCS#8</p>
<pre><code>-----BEGIN PRIVATE KEY-----

.

.

.

-----END PRIVATE KEY-----</code></pre></div>
</details>

<h3 id="자바에서-사용하는-경우-pkcs8-형식으로-변환-혹은-재생성을-하도록-한다">자바에서 사용하는 경우 pkcs#8 형식으로 변환 혹은 재생성을 하도록 한다.</h3>
<details>
<summary>pkcs#8형식으로 생성 <접기/펼치기></summary>
<div markdown="1">

<p>openssl 커맨드 </p>
<p><b># 개인키 pkcs#1 형식으로 생성</b><br/>
genrsa -out private_key1.pem 2048</p>
<p><b># 개인키 pkcs#1에서 pkcs#8 형식으로 변환</b><br/>
pkcs8 -in private_key1.pem -inform PEM -out private_key.pem -outform PEM -topk8 -nocrypt</p>
<p><b># 공개키 pkcs#8 형식으로 생성</b><br/>
rsa -in private_key.pem -out public_key.pem -pubout</p>
<p><img src="https://user-images.githubusercontent.com/66985977/209460522-f2bde580-e373-4170-b5f0-60921ecdebdf.png" alt="image"></p>
<p><img src="https://user-images.githubusercontent.com/66985977/209460535-1a71b1b2-79e4-458a-8e4b-6320fb043c87.png" alt="image">
<br/>(private_key1.pem은 사용하지 않음.) </p>
</div>
</details>

<hr>
]]></description>
        </item>
    </channel>
</rss>