Skip to content
Published on

NLP & テキスト処理完全ガイド: BERTファインチューニング、RAGシステム、多言語処理まで

Authors

NLP & テキスト処理完全ガイド: BERTファインチューニング、RAGシステム、多言語処理まで

自然言語処理(NLP)は、コンピュータが人間の言語を理解・生成できるようにするAIの中核技術です。このガイドでは、テキスト前処理の基礎からBERTファインチューニング、RAGパイプライン構築、多言語処理まで、実践的なコードとともに体系的に解説します。

目次

  1. テキスト前処理とトークン化
  2. サブワードトークン化: BPE、WordPiece、SentencePiece
  3. 単語埋め込み: Word2Vec、FastText、GloVe
  4. シーケンスモデルの進化: RNNからTransformerへ
  5. BERTファミリーモデルとファインチューニング
  6. テキストタスク実践
  7. RAGシステムの構築
  8. 多言語NLPと韓国語の特殊性
  9. クイズ

1. テキスト前処理とトークン化

1.1 韓国語形態素解析 (KoNLPy、MeCab)

韓国語は膠着語(agglutinative language)であり、語幹にさまざまな接辞が結合して意味を表します。英語のような空白ベースのトークン化では不十分です。KoNLPyは韓国語形態素解析のための代表的なPythonライブラリです。

from konlpy.tag import Okt, Mecab

okt = Okt()
text = "자연어 처리는 인공지능의 핵심 분야입니다."

# 形態素解析
morphs = okt.morphs(text)
print("形態素:", morphs)
# ['자연어', '처리', '는', '인공지능', '의', '핵심', '분야', '입니다', '.']

# 品詞タグ付け
pos_tags = okt.pos(text)
print("品詞タグ:", pos_tags)
# [('자연어', 'Noun'), ('처리', 'Noun'), ('는', 'Josa'), ...]

# 名詞の抽出
nouns = okt.nouns(text)
print("名詞:", nouns)
# ['자연어', '처리', '인공지능', '핵심', '분야']

MeCabは日本語形態素解析器を韓国語向けに移植したもので、速度と精度に優れています。大規模データ処理ではMeCabが推奨されます。

import re
from konlpy.tag import Okt

def preprocess_korean(text: str) -> list[str]:
    """韓国語テキスト前処理パイプライン"""
    # 特殊文字除去(ハングル、英数字のみ保持)
    text = re.sub(r'[^가-힣a-zA-Z0-9\s]', '', text)
    text = re.sub(r'\s+', ' ', text).strip()

    okt = Okt()
    # 名詞・動詞・形容詞のみ抽出(2文字以上)
    tokens = [
        word for word, pos in okt.pos(text)
        if pos in ['Noun', 'Verb', 'Adjective'] and len(word) > 1
    ]
    return tokens

sample = "자연어 처리 기술이 빠르게 발전하고 있습니다!"
print(preprocess_korean(sample))

1.2 ストップワード処理とテキスト正規化

KOREAN_STOPWORDS = [
    '이', '가', '을', '를', '은', '는', '의', '에', '에서',
    '로', '으로', '와', '과', '도', '만', '까지', '부터'
]

def remove_stopwords(tokens: list[str], stopwords: list[str]) -> list[str]:
    return [token for token in tokens if token not in stopwords]

def normalize_text(text: str) -> str:
    # 繰り返し文字の削減: 'ㅋㅋㅋㅋ' -> 'ㅋㅋ'
    text = re.sub(r'(.)\1{2,}', r'\1\1', text)
    text = text.lower()
    return text

2. サブワードトークン化: BPE、WordPiece、SentencePiece

2.1 BPE (Byte Pair Encoding)

BPEは最も頻出する文字ペアを繰り返し結合して語彙を構築します。OOV(Out-of-Vocabulary)問題を効果的に解決し、GPT系モデルで使用されます。

from tokenizers import Tokenizer, trainers, pre_tokenizers, models, decoders

# BPEトークナイザーの学習
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True)
tokenizer.decoder = decoders.ByteLevel()

trainer = trainers.BpeTrainer(
    vocab_size=30000,
    min_frequency=2,
    special_tokens=["[UNK]", "[PAD]", "[BOS]", "[EOS]"]
)

files = ["train_corpus.txt"]
tokenizer.train(files, trainer)

# トークン化の例
encoding = tokenizer.encode("自然言語処理はfascinatingです!")
print("トークン:", encoding.tokens)
print("ID:", encoding.ids)

# 保存と読み込み
tokenizer.save("bpe_tokenizer.json")
loaded_tokenizer = Tokenizer.from_file("bpe_tokenizer.json")

2.2 WordPiece と SentencePiece の比較

WordPiece(BERTで使用)はサブワードに ## プレフィックスを付けて連続性を表現します。SentencePieceは言語に依存しない方法で生テキストを処理します。

import sentencepiece as spm

# SentencePieceモデルの学習
spm.SentencePieceTrainer.train(
    input='corpus.txt',
    model_prefix='sp_model',
    vocab_size=32000,
    character_coverage=0.9995,  # 日本語・韓国語など高く設定
    model_type='bpe',
    pad_id=0,
    unk_id=1,
    bos_id=2,
    eos_id=3
)

sp = spm.SentencePieceProcessor()
sp.load('sp_model.model')

text = "自然言語処理はNLPの中核です。"
tokens = sp.encode_as_pieces(text)
print("SentencePieceトークン:", tokens)
# ['▁自然', '言語', '処理', 'は', 'NLP', 'の', '中核', 'です', '。']
方式使用モデル特徴
BPEGPT-2、RoBERTa頻度ベースのマージ、決定論的
WordPieceBERT、DistilBERT尤度最大化、## プレフィックス
SentencePieceT5、mBART、XLM-R言語非依存、空白込みで処理
Unigram LMALBERT、mBERT確率モデルベース、柔軟な分割

3. 単語埋め込み: Word2Vec、FastText、GloVe

3.1 Word2Vec

Word2VecはニューラルネットワークベースのWord埋め込み手法で、CBOW(Continuous Bag of Words)とSkip-gramの2つのアーキテクチャがあります。

from gensim.models import Word2Vec

corpus = [
    "自然言語処理は人工知能のコア分野です",
    "BERTは双方向トランスフォーマーモデルです",
    "テキスト分類と感情分析にBERTを活用します",
]

# 形態素解析後トークン化(日本語はMeCab等を使用)
tokenized_corpus = [sentence.split() for sentence in corpus]

# Word2Vec学習(Skip-gram)
model = Word2Vec(
    sentences=tokenized_corpus,
    vector_size=100,
    window=5,
    min_count=1,
    sg=1,        # 1: Skip-gram, 0: CBOW
    workers=4,
    epochs=100
)

# 類似語検索
similar_words = model.wv.most_similar('BERT', topn=5)
print("類似語:", similar_words)

# 単語ベクトルの取得
vector = model.wv['BERT']
print("ベクトルの形状:", vector.shape)  # (100,)

3.2 FastText と GloVe

FastTextは単語を文字n-gramの和として表現し、OOV単語も処理できます。GloVeはグローバル共起行列を活用した埋め込みです。

from gensim.models import FastText

ft_model = FastText(
    sentences=tokenized_corpus,
    vector_size=100,
    window=5,
    min_count=1,
    sg=1,
    min_n=2,
    max_n=6,
    epochs=100
)

# 未知語もn-gramで処理可能
oov_vector = ft_model.wv['自然言語AIエンジニア']
print("OOVベクトルの形状:", oov_vector.shape)

3.3 Sentence Transformers による文章埋め込み

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

# 多言語対応モデル
model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')

sentences = [
    "自然言語処理は人工知能の一分野です。",
    "NLPはAIの重要な領域です。",
    "今日の天気は晴れで気持ちがいいです。",
]

embeddings = model.encode(sentences)
print("埋め込みの形状:", embeddings.shape)  # (3, 768)

sim_matrix = cosine_similarity(embeddings)
print("類似度行列:")
print(sim_matrix.round(4))

# セマンティック検索
query = "言語モデルとは何ですか?"
query_embedding = model.encode([query])
similarities = cosine_similarity(query_embedding, embeddings)[0]

for sent, score in zip(sentences, similarities):
    print(f"スコア {score:.4f}: {sent}")

4. シーケンスモデルの進化: RNNからTransformerへ

4.1 RNN から LSTM、GRU への発展

import torch
import torch.nn as nn

class LSTMClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(
            embed_dim, hidden_dim,
            num_layers=2,
            batch_first=True,
            dropout=0.3,
            bidirectional=True
        )
        self.classifier = nn.Sequential(
            nn.Linear(hidden_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(hidden_dim, num_classes)
        )

    def forward(self, x):
        embedded = self.embedding(x)
        output, (hidden, cell) = self.lstm(embedded)
        # 双方向の最終隠れ状態を結合
        final_hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)
        return self.classifier(final_hidden)

class GRUClassifier(nn.Module):
    """GRUはLSTMよりパラメータが少なく学習が速い"""
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.gru = nn.GRU(
            embed_dim, hidden_dim,
            num_layers=2,
            batch_first=True,
            bidirectional=True
        )
        self.fc = nn.Linear(hidden_dim * 2, num_classes)

    def forward(self, x):
        embedded = self.embedding(x)
        _, hidden = self.gru(embedded)
        out = torch.cat([hidden[-2], hidden[-1]], dim=1)
        return self.fc(out)

4.2 マルチヘッドアテンション

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        attn_weights = torch.softmax(scores, dim=-1)
        return torch.matmul(attn_weights, V), attn_weights

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(key).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = self.W_v(value).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        attn_output, _ = self.scaled_dot_product_attention(Q, K, V, mask)
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        return self.W_o(attn_output)

RNN → LSTM/GRU(勾配消失問題の解決)→ Attention(長距離依存関係の捕捉)→ Transformer(並列処理と全方向アテンション)という発展により、NLPの性能が飛躍的に向上しました。


5. BERTファミリーモデルとファインチューニング

5.1 BERT事前学習タスク

BERT(Bidirectional Encoder Representations from Transformers)は2つの事前学習タスクを使用します。

  • MLM(Masked Language Model): 入力トークンの15%を [MASK] に置換し、元のトークンを予測
  • NSP(Next Sentence Prediction): 2つの文が実際に連続しているかどうかを二値分類

5.2 KLUE-BERT による韓国語テキスト分類

from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import TrainingArguments, Trainer
from datasets import Dataset
import numpy as np
from sklearn.metrics import accuracy_score, f1_score

# KLUE-BERTのロード
model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(
    model_name,
    num_labels=5  # 感情分類: 非常に否定的/否定的/中立/肯定的/非常に肯定的
)

train_data = {
    "text": [
        "이 영화 정말 재미있어요! 강력 추천합니다.",
        "서비스가 너무 불친절하고 품질도 별로예요.",
        "평범한 수준이네요. 특별한 점은 없었어요.",
    ],
    "label": [4, 0, 2]
}

def tokenize_function(examples):
    return tokenizer(
        examples["text"],
        max_length=128,
        padding="max_length",
        truncation=True
    )

dataset = Dataset.from_dict(train_data)
tokenized_dataset = dataset.map(tokenize_function, batched=True)

training_args = TrainingArguments(
    output_dir="./klue-bert-sentiment",
    num_train_epochs=3,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=32,
    warmup_ratio=0.1,
    weight_decay=0.01,
    learning_rate=2e-5,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="f1",
    fp16=True,
)

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return {
        "accuracy": accuracy_score(labels, predictions),
        "f1": f1_score(labels, predictions, average="weighted")
    }

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    eval_dataset=tokenized_dataset,
    compute_metrics=compute_metrics,
)
trainer.train()

5.3 KoELECTRA による NER ファインチューニング

from transformers import AutoTokenizer, AutoModelForTokenClassification
from transformers import pipeline

model_name = "monologg/koelectra-base-finetuned-ner"
tokenizer = AutoTokenizer.from_pretrained(model_name)
ner_model = AutoModelForTokenClassification.from_pretrained(model_name)

ner_pipeline = pipeline(
    "ner",
    model=ner_model,
    tokenizer=tokenizer,
    aggregation_strategy="simple"
)

text = "삼성전자 이재용 회장이 서울 강남구에서 기자회견을 열었습니다."
entities = ner_pipeline(text)

for entity in entities:
    print(f"エンティティ: {entity['word']}, タイプ: {entity['entity_group']}, "
          f"スコア: {entity['score']:.4f}")
# エンティティ: 삼성전자, タイプ: ORG, スコア: 0.9987
# エンティティ: 이재용, タイプ: PER, スコア: 0.9923
# エンティティ: 서울 강남구, タイプ: LOC, スコア: 0.9856

5.4 BERTファミリーモデルの比較

モデル主な改善点特徴
BERTベースラインMLM + NSP
RoBERTaNSP除去、より多くのデータ動的マスキング、大バッチ
DeBERTa分離アテンション位置とコンテンツを別々に処理
KLUE-BERT韓国語特化韓国語Wikipedia/ニュースで学習
KoELECTRAELECTRA方式Generator-Discriminator構造

6. テキストタスク実践

6.1 テキスト分類と感情分析

from transformers import pipeline

# 感情分析
sentiment = pipeline(
    "sentiment-analysis",
    model="distilbert-base-uncased-finetuned-sst-2-english"
)
result = sentiment("This product is amazing and worth every penny!")
print(result)  # [{'label': 'POSITIVE', 'score': 0.9998}]

# テキスト要約
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
long_text = """
Natural language processing (NLP) is a subfield of linguistics and artificial
intelligence. Recent advances in transformer-based models like BERT and GPT
have dramatically improved performance across many NLP tasks, approaching
or exceeding human-level performance in reading comprehension and translation.
"""
summary = summarizer(long_text, max_length=50, min_length=20)
print(summary[0]['summary_text'])

# 質問応答
qa = pipeline(
    "question-answering",
    model="deepset/roberta-base-squad2"
)
context = "Tokyo is the capital of Japan with a population of approximately 13.96 million."
question = "What is the population of Tokyo?"
answer = qa(question=question, context=context)
print(f"答え: {answer['answer']}, スコア: {answer['score']:.4f}")

6.2 ニューラル機械翻訳

from transformers import MarianMTModel, MarianTokenizer

# 日本語から英語への翻訳
model_name = "Helsinki-NLP/opus-mt-ja-en"
tokenizer = MarianTokenizer.from_pretrained(model_name)
model = MarianMTModel.from_pretrained(model_name)

def translate_ja_en(text: str) -> str:
    inputs = tokenizer(text, return_tensors="pt", padding=True)
    translated = model.generate(**inputs, num_beams=5)
    return tokenizer.decode(translated[0], skip_special_tokens=True)

japanese_text = "人工知能技術は私たちの生活を大きく変えています。"
print(f"翻訳: {translate_ja_en(japanese_text)}")

7. RAGシステムの構築

7.1 RAGパイプラインの概要

RAG(Retrieval-Augmented Generation)は、検索ベースで外部知識を活用してLLMの回答品質を高める手法です。文書チャンキング → 埋め込み → ベクトルストア → 検索 → 再ランキング → 生成の流れで構成されます。

7.2 LangChain と pgvector を使った RAG パイプライン

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import PGVector
from langchain_community.chat_models import ChatOllama
from langchain.chains import RetrievalQA
from langchain.schema import Document

# ステップ1: 文書のチャンキング
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ".", " ", ""],
    length_function=len,
)

documents = [
    Document(
        page_content="BERTは2018年にGoogleが発表した双方向トランスフォーマーモデルです...",
        metadata={"source": "nlp_guide.txt", "page": 1}
    )
]
chunks = text_splitter.split_documents(documents)
print(f"チャンク数: {len(chunks)}")

# ステップ2: 埋め込みモデルの設定(多言語対応)
embeddings = HuggingFaceEmbeddings(
    model_name="paraphrase-multilingual-mpnet-base-v2",
    model_kwargs={"device": "cuda"},
    encode_kwargs={"normalize_embeddings": True}
)

# ステップ3: pgvectorへの保存
CONNECTION_STRING = "postgresql+psycopg2://user:password@localhost:5432/ragdb"
COLLECTION_NAME = "nlp_documents"

vectorstore = PGVector.from_documents(
    documents=chunks,
    embedding=embeddings,
    collection_name=COLLECTION_NAME,
    connection_string=CONNECTION_STRING,
)

# ステップ4: 検索器の設定
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 5}
)

# ステップ5: RAGチェーンの構築
llm = ChatOllama(model="llama3.1:8b-instruct-q4_K_M")
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
)

query = "BERTの事前学習方法は何ですか?"
result = qa_chain.invoke({"query": query})
print(result["result"])

7.3 Two-Stage Retrieval: Bi-encoder + Cross-encoder

from sentence_transformers import SentenceTransformer, CrossEncoder
from sentence_transformers.util import cos_sim

# Stage 1: Bi-encoderによる高速候補検索
bi_encoder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')

# Stage 2: Cross-encoderによる精密な再ランキング
cross_encoder = CrossEncoder('cross-encoder/mmarco-mMiniLMv2-L12-H384-v1')

def two_stage_retrieval(query: str, corpus: list[str], top_k: int = 5) -> list[str]:
    """Bi-encoder + Cross-encoderの2段階検索"""
    # Stage 1: 高速な初期検索(上位20件)
    query_emb = bi_encoder.encode(query, convert_to_tensor=True)
    corpus_emb = bi_encoder.encode(corpus, convert_to_tensor=True)

    scores = cos_sim(query_emb, corpus_emb)[0]
    top_indices = scores.argsort(descending=True)[:20].tolist()
    candidates = [corpus[i] for i in top_indices]

    # Stage 2: Cross-encoderによる精密な再ランキング
    pairs = [[query, doc] for doc in candidates]
    rerank_scores = cross_encoder.predict(pairs)

    ranked = sorted(
        zip(candidates, rerank_scores),
        key=lambda x: x[1],
        reverse=True
    )
    return [doc for doc, _ in ranked[:top_k]]

corpus = [
    "BERTはマスク言語モデルで事前学習されます。",
    "GPTは自己回帰的な方法でテキストを生成します。",
    "今日の天気は晴れで気温は20度です。",
]
results = two_stage_retrieval("BERTの学習方法について教えてください", corpus)
for doc in results:
    print(f"- {doc}")

8. 多言語NLPと韓国語の特殊性

8.1 mBERT と XLM-R

from transformers import AutoTokenizer, AutoModel
import torch

# XLM-RoBERTa: 100言語対応
xlmr_tokenizer = AutoTokenizer.from_pretrained("xlm-roberta-base")
xlmr_model = AutoModel.from_pretrained("xlm-roberta-base")

sentences = {
    "韓国語": "자연어 처리는 인공지능의 핵심입니다.",
    "英語": "NLP is the core of artificial intelligence.",
    "日本語": "自然言語処理はAIの核心です。",
}

for lang, sentence in sentences.items():
    inputs = xlmr_tokenizer(
        sentence,
        return_tensors="pt",
        max_length=128,
        padding=True,
        truncation=True
    )
    with torch.no_grad():
        outputs = xlmr_model(**inputs)

    # [CLS]トークンの埋め込みを文章表現として使用
    sentence_embedding = outputs.last_hidden_state[:, 0, :]
    print(f"{lang}: 埋め込み形状 = {sentence_embedding.shape}")

8.2 CJK(中日韓)トークン化の特殊性

CJK言語は空白ベースの分割が機能しないため、特殊な処理が必要です。

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-multilingual-cased")

texts = {
    "韓国語": "자연어처리",       # 膠着語: 助詞・語尾が語幹に付く
    "中国語": "自然语言处理",       # 孤立語: 各文字が意味単位
    "日本語": "自然言語処理",       # 膠着語 + 漢字/ひらがな/カタカナ混用
    "英語":  "NaturalLanguageProcessing",
}

for lang, text in texts.items():
    tokens = tokenizer.tokenize(text)
    print(f"{lang}: {tokens}")

8.3 日本語NLPの特殊処理

# 日本語はMeCabやSudachiなどの形態素解析器を使用
# 以下はfugashiを使用した例

import fugashi

# fugashi: MeCabのPythonラッパー
tagger = fugashi.Tagger()

text = "自然言語処理技術が急速に発展しています。"

tokens = []
for word in tagger(text):
    print(f"単語: {word.surface}, 品詞: {word.feature.pos1}")
    if word.feature.pos1 in ['名詞', '動詞', '形容詞']:
        tokens.append(word.surface)

print("抽出トークン:", tokens)

# 韓国語との比較
# 韓国語の膠着語特性: 語幹 + 接辞の組み合わせが無限
# 日本語も膠着語だが、漢字・ひらがな・カタカナの3種類の文字体系が混在
# 韓国語はハングルのみ(漢字は現代文では少ない)

8.4 零照応(ゼロ照応)と談話処理

日本語・韓国語に多い省略表現への対応は多言語NLPの課題のひとつです。

# 零照応の例
# 「太郎は学校に行った。(彼は)宿題をした。」
# 英語: "Taro went to school. He did his homework."
# 日本語・韓国語では主語が省略されることが多い

# XLM-Rは多言語の転移学習でこのような特性をある程度吸収
from transformers import pipeline

# 多言語対応のNERで固有名詞を正確に識別
ner = pipeline(
    "ner",
    model="xlm-roberta-large-finetuned-conll03-english",
    aggregation_strategy="simple"
)

# 多言語テキストの処理
texts = [
    "Apple is headquartered in Cupertino, California.",
    "東京はアジア最大の都市の一つです。",
]

for text in texts:
    entities = ner(text)
    print(f"\nテキスト: {text}")
    for ent in entities:
        print(f"  {ent['word']} -> {ent['entity_group']} ({ent['score']:.3f})")

クイズ

Q1. BPEトークン化がOOV問題を解決する方法は?

答え: BPEは最も頻繁に出現する文字ペアを繰り返し結合してサブワード語彙を構築します。未知語もサブワード単位に分解できるため、OOVを最小化できます。

解説: 従来の単語ベース語彙では未知語を [UNK] で処理するため情報が失われます。BPEは文字単位から始めて頻出ペアをマージするため、新語や複合語も既存サブワードの組み合わせで表現できます。例えば「マルチリンガルNLP」は学習データになくても「マルチ」「リンガル」「NLP」などの単位に分解されます。

Q2. BERTのMLMとNSP事前学習タスクとは?

答え: MLMは入力トークンの15%をランダムにマスクし、元のトークンを予測するタスクです。NSPは2つの文が原文書で実際に連続しているかどうかを二値分類するタスクです。

解説: MLMにより双方向のコンテキスト学習が可能になります。マスクされた15%のうち80%は [MASK] に、10%はランダムトークンに、10%は元のトークンのまま維持されます。NSPは文間の関係を学習しますが、後続のRoBERTaなどではNSPが性能改善に寄与しないとして除去されました。

Q3. RAGにおいてbi-encoderとcross-encoderを組み合わせた2段階検索の利点は?

答え: Bi-encoderはクエリと文書を独立してエンコードし高速なANN検索を実現します。Cross-encoderはクエリ-文書ペアを一緒に処理して精密な関連性スコアを計算します。両者を組み合わせることで速度と精度を両立できます。

解説: Bi-encoderだけでは速いが精度が低く、cross-encoderは精度が高いが全文書ペアを計算する必要があるため遅いです。2段階方式ではbi-encoderで候補を絞り込み(20-100件)、その候補のみにcross-encoderを適用することで、精度と効率のバランスを実現します。

Q4. SBERTがBERTよりも文章類似度計算に効率的な理由は?

答え: SBERTはSiamese/Tripletネットワークで文章を固定サイズベクトルにエンコードします。事前計算されたベクトル間のコサイン類似度を計算するだけで済むため、毎回フォワードパスが必要なBERTより大幅に高速です。

解説: n文の全ペア類似度計算においてBERTはn(n-1)/2回のフォワードパスが必要(10,000文で約65時間)ですが、SBERTはn回のエンコード後にベクトル演算のみで済みます(約5秒)。意味検索、クラスタリングなど大規模比較が必要なタスクで特に有効です。

Q5. 韓国語NLPで膠着語の特性が英語と異なる処理を要求する理由は?

答え: 韓国語は語幹に助詞・語尾・接辞が結合する膠着語で、単語変形が非常に多様です。形態素解析によって語幹を抽出しなければ、「인공지능의」「인공지능이」「인공지능을」が別々の単語として扱われてしまいます。

解説: 英語は「eat」「eats」「eating」のように変形が単純で簡単なステミングで処理できます。一方、韓国語は一つの語幹に数百通りの接辞の組み合わせが可能なため、形態素解析器(MeCab、Oktなど)が必須です。日本語も膠着語ですが、漢字・ひらがな・カタカナが混在するという追加の複雑さがあります。


まとめ

このガイドでは、テキスト前処理からBPE/WordPiece/SentencePieceトークン化、Word2Vec/FastText/SBERTの埋め込み、RNNからTransformerへの進化、BERTファミリーのファインチューニング(KLUE-BERT、KoELECTRA)、実践的なNLPタスク(NER、感情分析、QA、要約、機械翻訳)、pgvectorとLangChainを使ったRAGパイプライン、2段階検索、XLM-Rを使った多言語処理まで幅広くカバーしました。

韓国語NLPの鍵は膠着語の特性を考慮した形態素解析であり、RAGシステムではbi-encoderの速度とcross-encoderの精度を組み合わせることで最高の検索品質を実現できます。CJK言語はそれぞれ固有の言語的特性を持つため、言語に合わせた前処理パイプラインの設計が重要です。