| """ |
| BERT 인터랙티브 데모 (한글 버전) |
| ================================ |
| Devlin et al. (2019), "BERT: Pre-training of Deep Bidirectional Transformers |
| for Language Understanding" (arXiv:1810.04805) 논문의 태스크들을 재현합니다. |
| |
| 데모는 논문의 구성을 따라 여섯 개 탭으로 나뉘어 있습니다: |
| |
| 사전학습 태스크 (논문 §3.1) |
| 1. Masked Language Model -- 핵심 양방향 목적함수 |
| 2. Next Sentence Prediction -- IsNext / NotNext 이진 태스크 |
| |
| 파인튜닝 태스크 (논문 §4, Figure 4) |
| 3. 문장 쌍 분류 -- MNLI / RTE / MRPC 계열 (Figure 4a) |
| 4. 단일 문장 분류 -- SST-2 / CoLA 계열 (Figure 4b) |
| 5. 질의응답 -- SQuAD v1.1 계열 (Figure 4c) |
| 6. 개체명 인식 -- CoNLL-2003 계열 (Figure 4d) |
| |
| 로컬 실행: |
| pip install -r requirements.txt |
| python app.py |
| """ |
|
|
| from __future__ import annotations |
|
|
| import gradio as gr |
| import torch |
| from transformers import ( |
| AutoTokenizer, |
| AutoModelForNextSentencePrediction, |
| pipeline, |
| ) |
|
|
| DEVICE = 0 if torch.cuda.is_available() else -1 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| _pipelines: dict[str, object] = {} |
|
|
|
|
| def get_pipeline(name: str): |
| """파이프라인을 캐시에서 반환하거나 처음 호출 시 빌드합니다.""" |
| if name in _pipelines: |
| return _pipelines[name] |
|
|
| if name == "mlm": |
| |
| _pipelines[name] = pipeline( |
| "fill-mask", |
| model="bert-base-uncased", |
| device=DEVICE, |
| top_k=5, |
| ) |
|
|
| elif name == "nsp": |
| |
| tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") |
| model = AutoModelForNextSentencePrediction.from_pretrained("bert-base-uncased") |
| model.eval() |
| if DEVICE >= 0: |
| model = model.cuda() |
| _pipelines[name] = (tokenizer, model) |
|
|
| elif name == "mnli": |
| |
| _pipelines[name] = pipeline( |
| "text-classification", |
| model="textattack/bert-base-uncased-MNLI", |
| device=DEVICE, |
| ) |
|
|
| elif name == "sst2": |
| |
| _pipelines[name] = pipeline( |
| "text-classification", |
| model="textattack/bert-base-uncased-SST-2", |
| device=DEVICE, |
| ) |
|
|
| elif name == "squad": |
| |
| _pipelines[name] = pipeline( |
| "question-answering", |
| model="bert-large-uncased-whole-word-masking-finetuned-squad", |
| device=DEVICE, |
| ) |
|
|
| elif name == "ner": |
| |
| _pipelines[name] = pipeline( |
| "token-classification", |
| model="dslim/bert-base-NER", |
| aggregation_strategy="simple", |
| device=DEVICE, |
| ) |
|
|
| else: |
| raise ValueError(f"알 수 없는 파이프라인입니다: {name}") |
|
|
| return _pipelines[name] |
|
|
|
|
| |
| |
| |
|
|
| def run_mlm(text: str) -> str: |
| """양방향 문맥을 이용해 [MASK] 토큰을 예측합니다.""" |
| if not text or "[MASK]" not in text: |
| return "❗ 문장에 `[MASK]` 토큰을 정확히 하나 포함시켜 주세요." |
|
|
| nlp = get_pipeline("mlm") |
| predictions = nlp(text) |
|
|
| |
| |
| if isinstance(predictions[0], list): |
| predictions = predictions[0] |
|
|
| lines = [ |
| f"**입력 문장:** `{text}`", |
| "", |
| "**상위 5개 예측 (softmax 확률 기준):**", |
| "", |
| ] |
| for i, pred in enumerate(predictions, 1): |
| score = pred["score"] |
| token = pred["token_str"] |
| sequence = pred["sequence"] |
| lines.append(f"{i}. **{token}** — 확률 `{score:.4f}`") |
| lines.append(f" → {sequence}") |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| def run_nsp(sentence_a: str, sentence_b: str) -> str: |
| """문장 B가 문장 A 다음에 실제로 등장하는지 분류합니다.""" |
| if not sentence_a.strip() or not sentence_b.strip(): |
| return "❗ 문장 A와 문장 B를 모두 입력해 주세요." |
|
|
| tokenizer, model = get_pipeline("nsp") |
| |
| |
| inputs = tokenizer(sentence_a, sentence_b, return_tensors="pt", truncation=True) |
| if DEVICE >= 0: |
| inputs = {k: v.cuda() for k, v in inputs.items()} |
|
|
| with torch.no_grad(): |
| outputs = model(**inputs) |
|
|
| |
| probs = torch.softmax(outputs.logits, dim=-1).squeeze().tolist() |
| is_next_prob, not_next_prob = probs[0], probs[1] |
| verdict = "✅ IsNext (이어지는 문장)" if is_next_prob > not_next_prob else "❌ NotNext (관련 없는 문장)" |
|
|
| return ( |
| f"**문장 A:** {sentence_a}\n\n" |
| f"**문장 B:** {sentence_b}\n\n" |
| f"**예측 결과:** {verdict}\n\n" |
| f"- P(IsNext) = `{is_next_prob:.4f}`\n" |
| f"- P(NotNext) = `{not_next_prob:.4f}`" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def run_mnli(premise: str, hypothesis: str) -> str: |
| """MNLI: 전제가 가설을 함의/모순/중립으로 만드는지 분류""" |
| if not premise.strip() or not hypothesis.strip(): |
| return "❗ 전제(premise)와 가설(hypothesis)을 모두 입력해 주세요." |
|
|
| nlp = get_pipeline("mnli") |
| |
| |
| result = nlp(f"{premise} [SEP] {hypothesis}")[0] |
| label = result["label"] |
| score = result["score"] |
|
|
| |
| label_kor = { |
| "entailment": "entailment (함의)", |
| "neutral": "neutral (중립)", |
| "contradiction": "contradiction (모순)", |
| "LABEL_0": "contradiction (모순)", |
| "LABEL_1": "entailment (함의)", |
| "LABEL_2": "neutral (중립)", |
| }.get(label, label) |
|
|
| return ( |
| f"**전제 (Premise):** {premise}\n\n" |
| f"**가설 (Hypothesis):** {hypothesis}\n\n" |
| f"**예측 결과:** `{label_kor}` (확신도 `{score:.4f}`)" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def run_sst2(text: str) -> str: |
| """SST-2 이진 감성 분류""" |
| if not text.strip(): |
| return "❗ 문장을 입력해 주세요." |
|
|
| nlp = get_pipeline("sst2") |
| result = nlp(text)[0] |
| label_map = {"LABEL_0": "😞 부정 (Negative)", "LABEL_1": "😀 긍정 (Positive)"} |
| label = label_map.get(result["label"], result["label"]) |
|
|
| return ( |
| f"**입력 문장:** {text}\n\n" |
| f"**감성:** {label}\n\n" |
| f"**확신도:** `{result['score']:.4f}`" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def run_squad(context: str, question: str) -> str: |
| """추출형 QA: context 안에서 답변 span을 찾아 반환""" |
| if not context.strip() or not question.strip(): |
| return "❗ 지문(context)과 질문(question)을 모두 입력해 주세요." |
|
|
| nlp = get_pipeline("squad") |
| |
| |
| result = nlp(question=question, context=context) |
|
|
| return ( |
| f"**질문:** {question}\n\n" |
| f"**답변:** **{result['answer']}**\n\n" |
| f"- 답변 위치: 문자 `{result['start']}`–`{result['end']}`\n" |
| f"- 확신도 점수: `{result['score']:.4f}`" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def run_ner(text: str) -> str: |
| """CoNLL-2003 NER: 각 토큰을 PER/ORG/LOC/MISC로 태깅""" |
| if not text.strip(): |
| return "❗ 분석할 문장을 입력해 주세요." |
|
|
| nlp = get_pipeline("ner") |
| entities = nlp(text) |
|
|
| if not entities: |
| return f"**입력 문장:** {text}\n\n_검출된 개체가 없습니다._" |
|
|
| |
| entity_kor = { |
| "PER": "인물 (PER)", |
| "ORG": "조직 (ORG)", |
| "LOC": "장소 (LOC)", |
| "MISC": "기타 (MISC)", |
| } |
|
|
| lines = [f"**입력 문장:** {text}", "", "**검출된 개체:**", ""] |
| for ent in entities: |
| kor_label = entity_kor.get(ent["entity_group"], ent["entity_group"]) |
| lines.append( |
| f"- **{ent['word']}** → `{kor_label}` " |
| f"(점수 `{ent['score']:.3f}`, 문자 {ent['start']}–{ent['end']})" |
| ) |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| INTRO_MD = """ |
| # 🤖 BERT 인터랙티브 데모 |
| |
| 이 Space는 **Devlin et al. (2019), *BERT: Pre-training of Deep Bidirectional Transformers |
| for Language Understanding*** ([arXiv:1810.04805](https://arxiv.org/abs/1810.04805)) 논문의 실험들을 |
| 직접 체험해보는 페이지입니다. |
| |
| 처음 두 탭은 논문의 **사전학습 목적함수(§3.1)** 를 보여줍니다: |
| - **Masked LM** — 무작위로 15% 토큰을 가리고 양방향 문맥으로 예측 |
| - **Next Sentence Prediction** — 문장 B가 문장 A 다음에 오는지 판단 |
| |
| 나머지 네 탭은 Figure 4의 **파인튜닝 태스크 카테고리**를 다룹니다: |
| - (a) 문장 쌍 분류 — MNLI |
| - (b) 단일 문장 분류 — SST-2 |
| - (c) 질의응답 — SQuAD v1.1 |
| - (d) 단일 문장 태깅 — CoNLL-2003 NER |
| |
| > 💡 각 탭을 처음 사용할 때 사전학습 체크포인트가 Hub에서 다운로드됩니다(약 400 MB – 1.3 GB). |
| > 그 이후 호출은 빠릅니다. |
| > |
| > 📝 사용된 모델은 모두 영어 BERT입니다. 한국어 BERT로 바꾸려면 `klue/bert-base` 등으로 모델 ID를 교체하세요. |
| """ |
|
|
|
|
| def build_ui() -> gr.Blocks: |
| with gr.Blocks(title="BERT 데모", theme=gr.themes.Soft()) as demo: |
| gr.Markdown(INTRO_MD) |
|
|
| with gr.Tabs(): |
| |
| with gr.Tab("1️⃣ Masked LM (사전학습)"): |
| gr.Markdown( |
| "문장 어디에든 `[MASK]` 토큰을 넣어보세요. 모델이 **왼쪽과 오른쪽 문맥을 모두** " |
| "사용하여 그 자리에 올 가장 가능성 높은 단어들을 예측합니다." |
| ) |
| mlm_in = gr.Textbox( |
| label="[MASK]가 포함된 문장", |
| value="The capital of France is [MASK].", |
| lines=2, |
| ) |
| mlm_out = gr.Markdown() |
| gr.Button("예측하기", variant="primary").click(run_mlm, mlm_in, mlm_out) |
| gr.Examples( |
| examples=[ |
| "The capital of France is [MASK].", |
| "I went to the bank to deposit my [MASK].", |
| "Albert Einstein was a famous [MASK].", |
| "She opened the door and [MASK] inside.", |
| ], |
| inputs=mlm_in, |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| |
| with gr.Tab("2️⃣ Next Sentence Prediction (사전학습)"): |
| gr.Markdown( |
| "두 문장이 주어졌을 때, 두 번째 문장이 첫 번째 문장 뒤에 실제로 이어지는 " |
| "문장인지(`IsNext`), 아니면 코퍼스에서 무작위로 뽑힌 관련 없는 문장인지" |
| "(`NotNext`)를 판단합니다." |
| ) |
| with gr.Row(): |
| nsp_a = gr.Textbox(label="문장 A", value="The man went to the store.", lines=2) |
| nsp_b = gr.Textbox(label="문장 B", value="He bought a gallon of milk.", lines=2) |
| nsp_out = gr.Markdown() |
| gr.Button("예측하기", variant="primary").click(run_nsp, [nsp_a, nsp_b], nsp_out) |
| gr.Examples( |
| examples=[ |
| ["The man went to the store.", "He bought a gallon of milk."], |
| ["The man went to the store.", "Penguins are flightless birds."], |
| ["She studied all night for the exam.", "She felt confident the next morning."], |
| ], |
| inputs=[nsp_a, nsp_b], |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| |
| with gr.Tab("3️⃣ 문장 쌍 분류 — MNLI"): |
| gr.Markdown( |
| "**Multi-Genre Natural Language Inference (MNLI).** " |
| "**전제(premise)** 와 **가설(hypothesis)** 이 주어지면, 둘의 관계를 " |
| "**함의(entailment)**, **중립(neutral)**, **모순(contradiction)** 중 하나로 분류합니다. " |
| "논문 Figure 4(a)에 해당합니다." |
| ) |
| with gr.Row(): |
| mnli_p = gr.Textbox( |
| label="전제 (Premise)", |
| value="A man inspects the uniform of a figure in some East Asian country.", |
| lines=2, |
| ) |
| mnli_h = gr.Textbox(label="가설 (Hypothesis)", value="The man is sleeping.", lines=2) |
| mnli_out = gr.Markdown() |
| gr.Button("분류하기", variant="primary").click(run_mnli, [mnli_p, mnli_h], mnli_out) |
| gr.Examples( |
| examples=[ |
| ["A soccer game with multiple males playing.", "Some men are playing a sport."], |
| ["A man is playing a guitar.", "A man is sleeping."], |
| ["The dog is running through the field.", "The animal is moving."], |
| ], |
| inputs=[mnli_p, mnli_h], |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| |
| with gr.Tab("4️⃣ 단일 문장 분류 — SST-2"): |
| gr.Markdown( |
| "**Stanford Sentiment Treebank (SST-2).** 영화 리뷰 문장의 감성을 " |
| "**긍정 / 부정** 이진 분류합니다. 논문 Figure 4(b)에 해당합니다." |
| ) |
| sst_in = gr.Textbox( |
| label="문장", |
| value="This movie was absolutely fantastic — I loved every minute of it.", |
| lines=2, |
| ) |
| sst_out = gr.Markdown() |
| gr.Button("감성 분석", variant="primary").click(run_sst2, sst_in, sst_out) |
| gr.Examples( |
| examples=[ |
| "This movie was absolutely fantastic — I loved every minute of it.", |
| "What a complete waste of time and money.", |
| "The cinematography was breathtaking and the score was sublime.", |
| "I have never been so bored in my entire life.", |
| ], |
| inputs=sst_in, |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| |
| with gr.Tab("5️⃣ 질의응답 — SQuAD v1.1"): |
| gr.Markdown( |
| "**Stanford Question Answering Dataset (SQuAD v1.1).** 지문과 질문이 주어지면, " |
| "모델이 지문 안에서 답이 시작되는 위치와 끝나는 위치, 즉 **답변 span** 을 예측합니다. " |
| "논문 Figure 4(c)에 해당합니다." |
| ) |
| squad_ctx = gr.Textbox( |
| label="지문 (Context)", |
| value=( |
| "BERT was introduced by researchers at Google AI Language in " |
| "October 2018. It stands for Bidirectional Encoder Representations " |
| "from Transformers and is pre-trained on the BooksCorpus (800M " |
| "words) and English Wikipedia (2,500M words). BERT-Large has 340 " |
| "million parameters." |
| ), |
| lines=6, |
| ) |
| squad_q = gr.Textbox(label="질문 (Question)", value="How many parameters does BERT-Large have?") |
| squad_out = gr.Markdown() |
| gr.Button("답변 찾기", variant="primary").click(run_squad, [squad_ctx, squad_q], squad_out) |
| gr.Examples( |
| examples=[ |
| [ |
| "BERT was introduced by researchers at Google AI Language in October 2018. It stands for Bidirectional Encoder Representations from Transformers and is pre-trained on the BooksCorpus (800M words) and English Wikipedia (2,500M words). BERT-Large has 340 million parameters.", |
| "When was BERT introduced?", |
| ], |
| [ |
| "BERT was introduced by researchers at Google AI Language in October 2018. It stands for Bidirectional Encoder Representations from Transformers and is pre-trained on the BooksCorpus (800M words) and English Wikipedia (2,500M words). BERT-Large has 340 million parameters.", |
| "What does BERT stand for?", |
| ], |
| ], |
| inputs=[squad_ctx, squad_q], |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| |
| with gr.Tab("6️⃣ 개체명 인식 — CoNLL-2003"): |
| gr.Markdown( |
| "**CoNLL-2003 NER.** 각 토큰을 인물(PER), 조직(ORG), 장소(LOC), " |
| "기타(MISC) 카테고리로 분류하는 토큰 단위 태깅 태스크입니다. " |
| "논문 Figure 4(d)에 해당합니다." |
| ) |
| ner_in = gr.Textbox( |
| label="문장", |
| value="Jacob Devlin works at Google in Mountain View, California.", |
| lines=2, |
| ) |
| ner_out = gr.Markdown() |
| gr.Button("개체 인식", variant="primary").click(run_ner, ner_in, ner_out) |
| gr.Examples( |
| examples=[ |
| "Jacob Devlin works at Google in Mountain View, California.", |
| "Apple CEO Tim Cook announced the new iPhone in Cupertino.", |
| "Angela Merkel met Emmanuel Macron in Berlin last Tuesday.", |
| ], |
| inputs=ner_in, |
| label="예시 (클릭해서 사용)", |
| ) |
|
|
| gr.Markdown( |
| "---\n" |
| "📄 논문: [arXiv:1810.04805](https://arxiv.org/abs/1810.04805) • " |
| "💻 원 저자 코드: [google-research/bert](https://github.com/google-research/bert) • " |
| "🤗 모델: [bert-base-uncased](https://huggingface.co/bert-base-uncased)" |
| ) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| build_ui().launch() |
|
|