NLP 上級技術

私たちは実は毎日 NLP と関わっています。スマートフォンの入力アプリで文章を打つときの予測変換、メールアプリが自動でフィルタリングするスパムメール、翻訳ソフトが外国語のウェブサイトを理解するのを助けてくれる——これらはすべて自然言語処理(Natural Language Processing、NLP)の応用です。

ChatGPT が登場する前、NLP は数十年にわたって発展してきましたが、技術的なハードルは非常に高かったのです。形態素解析、品詞タグ付け、構文解析、意味役割ラベリングなど、長い一連の概念を理解し、さらに手作業で特徴を設計して初めて、機械に少しでも言語を「理解」させることができました。

今日、大規模言語モデルによってこれらのことはすべて簡単になりました。しかし NLP の技術的な流れを理解することで、大規模モデルがなぜこれらのことを実現できるのか、そしてその限界がどこにあるのかを、より深く理解できます。

このモジュールでは、単語ベクトルから始めて、現在の大規模言語モデルに至るまで、NLP の完全な知識体系を構築します。。

学習パス:単語ベクトル → 事前学習モデル → BERT/GPT/T5 の三大パラダイム → 下流タスク(分類、NER、翻訳、要約)。各ステップには対応するコード例があり、理論を実践に落とし込むのに役立ちます。


事前学習言語モデルの進化史

NLP の発展はいくつかの段階に明確に分けることができ、各段階には象徴的な技術的ブレークスルーがあります。

Word2Vec:単語ベクトルの革命

2013 年以前、コンピュータがテキストを処理する方法は非常に原始的でした。

通常は「One-Hot エンコーディング」が使われていました。各単語は非常に長いベクトルに対応し、1 つの位置だけが 1 で、他はすべて 0 です。例えば、語彙が 10 万語ある場合、各単語は 10 万次元のベクトルになります。

この方法の問題は明らかです。ベクトルには意味情報がなく、「猫」と「犬」の距離は、「猫」と「机」の距離と同じくらい遠いのです。

Word2Vec の核となる考え方は次のとおりです:ある単語の意味は、その周囲の単語によって定義される。

例

# ============================================
# Word2Vec 基本概念のデモ
# gensim ライブラリを使って簡単な単語ベクトルモデルを訓練する
# ============================================

# まず gensim をインストール:pip install gensim

from gensim.models import Word2Vec
import numpy as np

# 訓練データを準備:いくつかの簡単な文
sentences = [
    ["我", "喜欢", "吃", "苹果"],
    ["我", "喜欢", "吃", "香蕉"],
    ["猫", "喜欢", "吃", "鱼"],
    ["狗", "喜欢", "吃", "肉"],
    ["苹果", "だ", "一种", "水果"],
    ["香蕉", "だ", "一种", "水果"],
    ["猫", "だ", "一种", "动物"],
    ["狗", "だ", "一种", "动物"],
    ["サンプル", "だ", "一个", "编程", "网站"],
    ["学习", "编程", "去", "サンプル"],
]

# 訓練 Word2Vec モデル
# vector_size: 単語ベクトルの次元
# window: コンテキストウィンドウサイズ(前後の単語をいくつ見るか)
# min_count: 出現回数がこの値未満の単語を無視する
# workers: 並列トレーニングのスレッド数
model = Word2Vec(
    sentences=sentences,
    vector_size=50,  # 各単語を 50 次元ベクトルで表す
    window=3,       # 前後 3 語を見る
    min_count=1,    # すべての単語を保持する
    workers=4,
    epochs=100      # 100 エポック訓練する
)

# 単語ベクトルを取得
apple_vector = model.wv["苹果"]
print(f'りんご' の単語ベクトル(前10次元):{apple_vector[:10]})
print(f"词向量维度:{len(apple_vector)}")

# 単語間の類似度を計算
similarity = model.wv.similarity("苹果", "香蕉")
print(f'リンゴ' と 'バナナ' の類似度:{similarity:.4f})

similarity = model.wv.similarity("苹果", "猫")
print(f'リンゴ' と '猫' の類似度:{similarity:.4f})

# 最も類似した単語を探す
print("\n'猫'に最も似ている単語:”)
for word, score in model.wv.most_similar("猫", topn=3):
    print(f"  {word}: {score:.4f}")

print("\n'サンプル' に最も似ている語:")
for word, score in model.wv.most_similar("サンプル", topn=3):
    print(f"  {word}: {score:.4f}")

# 古典的な単語ベクトル演算:王 - 男 + 女 ≈ 女王
# この小さなコーパスで試してみよう:果物 - リンゴ + 魚 ≈ ?
if "苹果" in model.wv and "鱼" in model.wv and "水果" in model.wv:
    result = model.wv.most_similar(positive=["水果", "鱼"], negative=["苹果"], topn=3)
    print("\n'水果' - '苹果' + '鱼' ≈")
    for word, score in result:
        print(f"  {word}: {score:.4f}")

Word2Vec の成功は次の一点を証明しました:意味はベクトル空間で表現できる。

しかし、それには限界があります。各単語は文脈に関係なく、固定された 1 つのベクトルしか持ちません。例えば「打」という字は「打电话(電話をかける)」と「打游戏(ゲームをする)」では異なる意味ですが、Word2Vec は同じベクトルを与えます。

ELMo:文脈依存の単語ベクトル

2018 年に登場した ELMo(Embeddings from Language Models)がこの問題を解決しました。

ELMo の考え方は次のとおりです:各単語にあらかじめ固定ベクトルを与えるのではなく、文全体を考慮してから、その単語のベクトルを生成します。

同じ「打」という字でも、「我打电话(私は電話をかける)」では一つのベクトル、「我打游戏(私はゲームをする)」では別のベクトルになります。

ELMo は双方向 LSTM(長短期記憶ネットワーク)を使用して文脈をモデル化しました。これは「事前学習 + ファインチューニング」というパラダイムを初めて大規模に使用したものです。

GPT-1:一方向事前学習

同じく 2018 年、OpenAI は GPT-1(Generative Pre-training Transformer)を発表しました。

その特徴は次のとおりです:

1. LSTM ではなく Transformer デコーダーを使用

2. 一方向:前の単語だけを見て、次の単語を予測する

3. 生成型:テキストを続けて生成できる

GPT-1 は、Transformer が NLP タスクにおいて大きな可能性を秘めていることを証明しました。

BERT:双方向事前学習

2018年末、GoogleはBERT(Bidirectional Encoder Representations from Transformers)を公開し、NLP分野を一変させました。

BERTの中心的な革新は次の通りです:

1. 双方向:前後の文脈を同時に見る

2. MLM(Masked Language Model):ランダムにいくつかの単語をマスクし、モデルに予測させる

3. NSP(Next Sentence Prediction):2つの文が連続しているかどうかを判定する

BERTは11のNLPタスクで当時最高の成績を達成し、NLPが「事前学習モデル時代」に入ったことを示しました。

GPT-3 から ChatGPT への躍進

2020年、GPT-3が公開され、パラメータ数は1750億に達しました。

人々は、モデルが十分に大きく、データが十分に多いと、「創発」(Emergence)現象が現れることを発見しました。モデルが突然、小規模モデルにはない能力(Few-shot学習、複雑な推論など)を備えるのです。

2022年末、ChatGPTが公開され、RLHF(人間のフィードバックによる強化学習)によってモデルの出力が人間の好みに合うようになり、AIは真に大衆のものとなりました。

この進化の歴史を表でまとめましょう:

年モデル中心的な考え方歴史的な位置づけ
2013Word2Vec単語の周囲の単語を使ってその意味を定義する単語ベクトル革命、意味のベクトル化表現
2018ELMo文脈に依存した単語ベクトル初めて多義語のベクトル表現を実現
2018GPT-1Transformerデコーダー、一方向の事前学習Transformerの可能性を証明
2018BERTTransformerエンコーダー、双方向の事前学習NLPは事前学習モデル時代へ
2020GPT-31750億パラメータ、創発能力大規模モデルの無限の可能性を示す
2022ChatGPTRLHF+対話能力AIは真に大衆のものへ

BERT 徹底解説

BERTはNLP発展史におけるマイルストーンであり、深く理解する価値があります。

MLM(マスク言語モデル)タスク

BERTの中心的な事前学習タスクはMLMです。文中の15%の単語をランダムに[MASK]に置き換え、モデルに元の単語が何かを予測させます。

例えば文:「猫が好きだ」は「猫が[MASK]だ」になるかもしれません。モデルは[MASK]が「好き」だと予測する必要があります。

なぜそうするのか? これにより、モデルは左右どちらかだけでなく、前後の文脈情報を同時に利用せざるを得なくなるからです。

実際の運用では、この15%の単語はすべて[MASK]に置き換えるわけではなく、次のようにします:

1. 80%の確率で[MASK]に置き換える

2. 10%の確率でランダムな単語に置き換える

3. 10%の確率で元の単語のままにする

こうすることでモデルがより頑健になります。ある単語が置き換えられたかどうかを確信できないため、文脈を本当に理解する必要があるのです。

NSP(次文予測)タスク

BERTの2つ目の事前学習タスクはNSPです。2つの文AとBを与え、BがAの後に続く本当の次の文かどうかを判定します。

正例:A=「猫が好きだ」、B=「猫はかわいい」

負例:A=「猫が好きだ」、B=「今日は天気がいい」

このタスクはモデルが文間の関係を理解するのに役立ち、質問応答や自然言語推論などのタスクに非常に有効です。

BERT の適用シナリオ

BERTは「エンコーダー」アーキテクチャで、言語理解に優れており、以下のようなタスクに適しています:

テキスト分類、感情分析、固有表現認識、質問応答、自然言語推論など。

テキスト生成はあまり得意ではなく、それはGPTの強みです。

BERT をファインチューニングする標準的な手順

Hugging Face Transformersライブラリを使って、BERTをファインチューニングしてテキスト分類を行う実践をしましょう:

例

# ============================================
# Hugging Faceを使ってBERTをファインチューニングしてテキスト分類を行う
# タスク:文がポジティブかネガティブかを判定する
# ============================================

# まず依存関係をインストールします:
# pip install transformers datasets torch scikit-learn

import torch
from transformers import (
    BertTokenizer,
    BertForSequenceClassification,
    TrainingArguments,
    Trainer,
)
from datasets import Dataset, load_metric
import numpy as np
from sklearn.model_selection import train_test_split

# ============================================
# 1. データの準備
# ============================================

# 簡単な感情分類データセットを構築する
texts = [
    "この製品は本当に使いやすく、とても気に入っています",
    "品質がひどすぎて、とてもがっかりしました",
    "サンプルチュートリアルはとてもわかりやすく、学びやすいです",
    "配送が遅く、包装も破れていました",
    "このスマホのカメラ性能は素晴らしい",
    "サービス態度が悪く、もう来ません",
    "本の内容は素晴らしく、購入をおすすめします",
    "映画は退屈で、見ながら寝てしまいました",
    "このレストランの料理はとてもおいしい",
    "ゲームはバグが多すぎて、体験がとても悪い",
    "サンプルのNLPチュートリアルはとても役立ちました",
    "このソフトウェアのUIデザインはよくできています",
    "配達員の態度はとても親切です",
    "服のサイズが合わず、色も違います",
    "ホテルの環境は静かで、よく眠れました",
    "カスタマーサポートの返信が遅く、問題が解決しませんでした",
]

labels = [
    1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0
]  # 1=ポジティブ、0=ネガティブ

# トレーニングセットと検証セットに分割
train_texts, val_texts, train_labels, val_labels = train_test_split(
    texts, labels, test_size=0.25, random_state=42
)

# Datasetオブジェクトを作成
train_dataset = Dataset.from_dict({"text": train_texts, "label": train_labels})
val_dataset = Dataset.from_dict({"text": val_texts, "label": val_labels})

# ============================================
# 2. tokenizerとモデルを読み込む
# ============================================

# 中国語BERTモデルを使用
model_name = "bert-base-chinese"

tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2  # 2値分類タスク
)

# ============================================
# 3. データ前処理:テキストをモデル入力に変換
# ============================================

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

# 前処理を適用
tokenized_train = train_dataset.map(tokenize_function, batched=True)
tokenized_val = val_dataset.map(tokenize_function, batched=True)

# ============================================
# 4. 評価指標を定義
# ============================================

metric = load_metric("accuracy")

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

# ============================================
# 5. トレーニングパラメータを設定
# ============================================

training_args = TrainingArguments(
    output_dir="./サンプル-bert-sentiment",  # 出力ディレクトリ
    num_train_epochs=3,                    # トレーニングエポック数
    per_device_train_batch_size=4,         # トレーニングのバッチサイズ
    per_device_eval_batch_size=4,          # 評価のバッチサイズ
    warmup_steps=5,                        # ウォームアップステップ数
    weight_decay=0.01,                     # 重み減衰
    logging_dir="./logs",                  # ログディレクトリ
    logging_steps=10,
    evaluation_strategy="epoch",           # 各エポックで1回評価
    save_strategy="epoch",
    learning_rate=2e-5,
    load_best_model_at_end=True,
)

# ============================================
# 6. Trainerを作成してトレーニングを開始
# ============================================

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_train,
    eval_dataset=tokenized_val,
    compute_metrics=compute_metrics,
)

# トレーニングを開始(時間とGPUが必要なためコメントアウト)
# print("トレーニングを開始...")
# trainer.train()

# ============================================
# 7. トレーニング済みモデルを使って予測
# ============================================

# ここでは元のモデルを直接使って予測の流れをデモします
# 実際に使うときは、trainerでトレーニングして保存したモデルを使うべきです

def predict_sentiment(text, model, tokenizer):
    """文の感情を予測"""
    inputs = tokenizer(
        text,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=64
    )

    model.eval()
    with torch.no_grad():
        outputs = model(**inputs)
        logits = outputs.logits
        probabilities = torch.softmax(logits, dim=-1)
        prediction = torch.argmax(probabilities, dim=-1).item()

    label_map = {0: "ネガティブ", 1: "ポジティブ"}
    confidence = probabilities[0][prediction].item()

    return {
        "text": text,
        "sentiment": label_map[prediction],
        "confidence": confidence,
        "negative_prob": probabilities[0][0].item(),
        "positive_prob": probabilities[0][1].item(),
    }

# いくつかの文をテスト
test_texts = [
    "サンプルチュートリアルは本当に素晴らしい!",
    "この製品の品質はひどすぎる",
    "今日は天気が本当に良い",
    "この映画はあまり好きではない",
]

print("モデル予測のテスト(事前学習済みのbert-base-chineseを使用):")
print("-" * 50)
for text in test_texts:
    # 注意:ここでは元のモデルを直接使用しており、ファインチューニングしていないため、結果はデモ用です
    result = predict_sentiment(text, model, tokenizer)
    print(f"テキスト:{result['text']}")
    print(f"感情:{result['sentiment']}")
    print(f"信頼度:{result['confidence']:.4f}")
    print(f"ポジティブ確率:{result['positive_prob']:.4f}")
    print(f"ネガティブ確率:{result['negative_prob']:.4f}")
    print("-" * 50)

注意:上記のコードは完全な流れを示していますが、実際にBERTをファインチューニングするにはGPUと時間がかかります。本番環境では、より軽量なモデル(distilbertなど)を使うか、大規模モデルのAPIを直接使うことも検討できます。


GPT シリーズ徹底解説

GPT(Generative Pre-trained Transformer)はBERTとは異なる道を歩みました。

自己回帰言語モデル

GPTは「自己回帰」です。単語を1つずつ生成し、各ステップでそれまでに生成したすべての単語を使って次の単語を予測します。

例えば「猫が好きだ」を生成する場合、過程は次のようになります:

1. 「猫」を入力し、次の単語が「が」だと予測

2. 「猫が」を入力し、次の単語が「好き」だと予測

3. 「猫が好き」を入力し、次の単語が「だ」だと予測

この方法は本質的にテキスト生成に適しています。

Decoder-Only アーキテクチャ

GPTはTransformerのデコーダーのみを使用し、BERTはエンコーダーのみを使用します。

デコーダーの特徴は「マスク付き自己注意」(Masked Self-Attention)を持つことです。各位置は左側の位置しか見ることができず、右側を見ることはできません。

これは非常に合理的です。テキスト生成は左から右へ行うため、まだ生成されていない単語を覗き見ることはできないからです。

Scaling Law の発見

GPTシリーズで最も重要な発見はスケーリング則(Scaling Law)です。モデルの性能は、モデルサイズ・データ量・計算量とべき乗則の関係にあります。。

簡単に言うと、モデルが大きく、データが多く、学習時間が長いほど、効果は良くなります。しかもこの向上は予測可能で、明確な頭打ち(プラトー)はありません。

だからこそGPTシリーズは「大きくする」ことを続けてきました。GPT-1(1.17億)→ GPT-2(15億)→ GPT-3(1750億)です。


T5 と Seq2Seq パラダイム

もうひとつの第三のパラダイムとして、エンコーダー-デコーダー(Encoder-Decoder)構造があり、T5はその代表です。

テキストからテキストへの統一フレームワーク

T5(Text-to-Text Transfer Transformer)の核となる考え方は次です。すべてのNLPタスクを「テキストからテキスト」という形式に統一する。

例:

テキスト分類:入力「分類:この映画はとても面白い」→ 出力「ポジティブ」

翻訳:入力「英語から日本語に翻訳:Hello world」→ 出力「こんにちは世界」

要約:入力「要約:これは...についての記事です」→ 出力「本記事は主に...について述べています」

この統一フレームワークの利点は、同じモデルでさまざまなタスクを処理でき、タスクごとにモデル構造を変更する必要がないことです。

エンコーダー-デコーダーの動作方式

エンコーダーは入力テキストの理解を担当し、デコーダーは出力テキストの生成を担当します。

翻訳を例にすると:

1. エンコーダーが「Hello world」を読み取り、「理解」した後の表現を生成する

2. デコーダーはこの表現に基づいて、単語をひとつずつ「こんにちは世界」を生成する

デコーダーはエンコーダーの異なる位置に「注意」を向けることができます。例えば、「こんにちは」を生成するときは「Hello」に、「世界」を生成するときは「world」により注目します。


三大パラダイム比較:BERT vs GPT vs T5

表でこれら3つのアーキテクチャを比較しましょう。

特性BERTGPTT5
アーキテクチャEncoder-onlyDecoder-onlyEncoder-Decoder
注意機構(アテンション)双方向(前後を見る)単方向(前のみを見る)エンコーダーは双方向、デコーダーは単方向
事前学習タスクMLM(マスク予測)自己回帰言語モデリングSpan Corruption
得意なタスク理解系:分類、NER、質問応答生成系:続きの生成、対話変換系:翻訳、要約、リライト
代表モデルBERT、RoBERTa、ALBERTGPT-1/2/3/4、ChatGPTT5、BART、mT5
代表的な応用感情分析、スパムメールフィルタリング文章作成アシスタント、対話ボット機械翻訳、文書要約

今日の大規模言語モデルの多くはDecoder-onlyアーキテクチャ(GPT路線)を採用しています。生成において最も優れた性能を示し、スケーリング則によって継続的に向上できるからです。ただし、特定のタスクでは、他の2つのアーキテクチャにも依然として利点があります。


テキスト分類

テキスト分類は最も一般的なNLPタスクの一つであり、初心者に最適な入門タスクでもあります。

多クラス分類の実践

多クラス分類とは、テキストにラベルを割り当てる作業で、ラベルには複数の選択肢があります。

例:ニュース分類(テクノロジー、スポーツ、エンタメ、経済)、カスタマーサービスのチケット分類、商品レビュー分類など。

例

# ============================================
# テキスト分類実践:複数の手法の比較
# ============================================

from typing import List, Dict, Any

# ============================================
# 手法1:従来の機械学習 + TF-IDF
# 小規模データセット向き、解釈可能性が高い
# ============================================

def traditional_text_classification_demo():
    """従来の手法でテキスト分類を行う"""
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.naive_bayes import MultinomialNB
    from sklearn.pipeline import Pipeline

    # 学習データ
    texts = [
        "今日、株を買ったら、たくさん上がった",
        "投資信託の積立は良い資産運用の方法だ",
        "このチームのフォワードがゴールを決めた",
        "NBAプレーオフは本当に面白い",
        "このスマホのカメラは写真がとてもきれいだ",
        "サンプルのプログラミングチュートリアルは分かりやすい",
        "この映画のストーリーは感動的だ",
        "あのアルバムの曲はとても良い",
    ]

    labels = ["経済", "経済", "スポーツ", "スポーツ", "テクノロジー", "テクノロジー", "エンタメ", "エンタメ"]

    # パイプライン作成:TF-IDF + ナイーブベイズ
    pipeline = Pipeline([
        ("tfidf", TfidfVectorizer()),  # テキストをTF-IDF特徴量に変換する
        ("classifier", MultinomialNB()),  # ナイーブベイズ分類器
    ])

    # 学習
    pipeline.fit(texts, labels)

    # テスト
    test_texts = [
        "株が下がって、気分がとても悪い",
        "バスケの試合でラスト1秒に逆転シュート",
        "新発売のノートパソコンは性能が高い",
        "サンプルの新しいNLPチュートリアル",
    ]

    predictions = pipeline.predict(test_texts)
    probabilities = pipeline.predict_proba(test_texts)

    print("手法1:従来の機械学習 + TF-IDF")
    print("-" * 50)
    for text, pred, probs in zip(test_texts, predictions, probabilities):
        print(f"テキスト:{text}")
        print(f"予測:{pred}")
        print(f"各クラスの確率:{dict(zip(pipeline.classes_, probs))}")
        print()

# ============================================
# 手法2:Hugging Faceパイプラインを使用
# 素早いプロトタイプ向け、学習不要
# ============================================

def huggingface_pipeline_demo():
    """Hugging Faceの事前学習済みパイプラインを使用"""
    print("手法2:Hugging Faceパイプライン")
    print("-" * 50)

    try:
        from transformers import pipeline

        # 感情分析パイプラインを使用(英語)
        classifier = pipeline("sentiment-analysis")

        test_texts = [
            "I love サンプル tutorials!",
            "This movie is terrible.",
        ]

        results = classifier(test_texts)

        for text, result in zip(test_texts, results):
            print(f"テキスト:{text}")
            print(f"感情:{result['label']}")
            print(f"信頼度:{result['score']:.4f}")
            print()

    except Exception as e:
        print(f"先にtransformersをインストールする必要があります:pip install transformers")
        print(f"エラー:{e}")

# ============================================
# 手法3:大規模モデルAPIを使用(推奨)
# 本番環境向き、効果が最も高い
# ============================================

def llm_based_classification_demo():
    """大規模モデルでテキスト分類を行う(シミュレーション)"""
    print("手法3:大規模モデルによる分類(デモの考え方)")
    print("-" * 50)

    # 実際に使うときはOpenAI/AnthropicなどのAPIを呼び出す
    # ここでは分類のプロンプト設計をデモする

    def classify_with_llm(text: str, categories: List[str]) -> Dict[str, Any]:
        """LLMで分類を行うシミュレーション関数"""
        # 実運用:LLM APIを呼び出す
        # prompt = f"""次のテキストを分類して、カテゴリ名だけを返してください。
        # テキスト:{text}
        # 選択可能なカテゴリ:{', '.join(categories)}"""

        # ここでは簡単なキーワードマッチングでシミュレーションする
        category_map = {
            "経済": ["株", "投資信託", "資産運用", "投資"],
            "スポーツ": ["バスケットボール", "サッカー", "試合", "ゴール"],
            "テクノロジー": ["スマートフォン", "パソコン", "プログラミング", "チュートリアル"],
            "エンタメ": ["映画", "音楽", "アルバム", "曲"],
        }

        for category, keywords in category_map.items():
            if any(keyword in text for keyword in keywords):
                return {
                    "category": category,
                    "confidence": 0.9,
                    "reasoning": f"含まれるキーワード:{', '.join([k for k in keywords if k in text])}"
                }

        return {"category": "その他", "confidence": 0.5}

    test_texts = [
        "株が上がって嬉しい",
        "サッカーワールドカップが開幕した",
        "サンプルの新しいプログラミングチュートリアル",
    ]

    categories = ["経済", "スポーツ", "テクノロジー", "エンタメ", "その他"]

    for text in test_texts:
        result = classify_with_llm(text, categories)
        print(f"テキスト:{text}")
        print(f"分類:{result['category']}")
        print(f"信頼度:{result['confidence']:.4f}")
        if "reasoning" in result:
            print(f"理由:{result['reasoning']}")
        print()

# ============================================
# デモを実行
# ============================================

if __name__ == "__main__":
    traditional_text_classification_demo()
    print("=" * 50)
    llm_based_classification_demo()

Few-shot 分類手法

Few-Shot学習(Few-Shot Learning)は大規模モデルがもたらした新しい能力です。いくつかの例を与えるだけで、モデルは分類を学習できます。

例:

テキスト:この製品はとても使いやすい → ラベル:ポジティブ

テキスト:品質がひどすぎる → ラベル:ネガティブ

テキスト:[あなたの新しいテキスト] → ラベル:?

大規模モデルはこの2つの例を見るだけでタスクを理解し、新しいテキストを適切に分類できます。

大規模モデル vs 小規模モデルの選択

どのようなときにどの手法を使うのか?ここに判断ガイドがあります。

シナリオ推奨手法理由
データ量が小さい(< 1000件)大規模モデルの少数サンプル学習小さいモデルでは学習できないが、大規模モデルは汎化能力が高い
データ量が大きい(> 10000件)小さいモデルのファインチューニング(例:BERT)コストが低く、推論が速い
迅速な検証が必要大規模モデルAPIトレーニング不要で、すぐに利用可能
本番環境、高スループット小さいモデルのファインチューニング + 蒸留推論速度が速く、コストを制御可能
カテゴリが頻繁に変わる大規模モデルのプロンプトエンジニアリング再トレーニング不要、プロンプトを変更するだけでよい

固有表現抽出(NER)

固有表現認識(Named Entity Recognition、NER)とは、テキストから特定のエンティティ(人名、地名、組織名など)を見つけ出すことです。

系列ラベリングフレームワーク

NERは通常、「系列ラベリング」タスクとしてモデル化されます。各単語に、それがエンティティの一部かどうかを示すラベルを付けます。

例えば文:「張三はGoogleで働いています」の場合、ラベルは次のようになります:

张三 → B-PER(人名の開始)

在 → O(非エンティティ)

Google → B-ORG(組織の開始)

工作 → O

BIO ラベル形式

最も一般的なラベル形式はBIOです:

B-XXX:エンティティの開始

I-XXX:エンティティの内部

O:非エンティティ

例:「王小明は北京のマイクロソフトで働いています」の場合、ラベルは次の通りです:

王 → B-PER

小 → I-PER

明 → I-PER

に → O

北 → B-LOC

京 → I-LOC

の → O

微 → B-ORG

软 → I-ORG

公 → I-ORG

司 → I-ORG

上 → O

班 → O

実践応用

Hugging Faceを使ってNERをやってみましょう:

例

# ============================================
# NER実践:Hugging Faceを使った固有表現認識
# ============================================

def ner_with_huggingface():
    """事前学習済みNERモデルを使用"""
    print("NER実践:Hugging Face")
    print("-" * 50)

    try:
        from transformers import pipeline

        # NERパイプラインをロード
        # 英語NER
        ner_pipeline = pipeline(
            "ner",
            model="dbmdz/bert-large-cased-finetuned-conll03-english",
            grouped_entities=True  # 同じエンティティの複数のトークンをマージ
        )

        test_text = "John Smith works at Google in New York City. He loves サンプル tutorials."

        results = ner_pipeline(test_text)

        print(f"テキスト:{test_text}")
        print("認識されたエンティティ:")
        for entity in results:
            print(f" - {entity['word']}: {entity['entity_group']} (信頼度: {entity['score']:.4f})")
        print()

    except Exception as e:
        print(f"先にtransformersをインストールする必要があります:pip install transformers")
        print(f"エラー:{e}")

# ============================================
# 中国語NER:正規表現 + キーワードを使用(単純なシナリオ)
# ============================================

def simple_chinese_ner():
    """簡易中国語NER:正規表現と辞書を使用"""
    import re

    print("簡易中国語NER:正規表現 + 辞書")
    print("-" * 50)

    # エンティティ辞書(実際のシナリオではもっと大きい)
    person_names = ["张三", "李四", "王小明", "刘德华"]
    organizations = ["Google", "微软", "阿里巴巴", "腾讯", "サンプル"]
    locations = ["北京", "上海", "深圳", "纽约", "伦敦"]

    # 正規表現パターン
    patterns = {
        "PERSON": "|".join(re.escape(name) for name in person_names),
        "ORG": "|".join(re.escape(org) for org in organizations),
        "LOC": "|".join(re.escape(loc) for loc in locations),
        "PHONE": r"1[3-9]\d{9}",  # 電話番号
        "EMAIL": r"\w+@\w+\.\w+",  # メールアドレス
    }

    def extract_entities(text: str):
        """テキストからエンティティを抽出"""
        entities = []

        for entity_type, pattern in patterns.items():
            for match in re.finditer(pattern, text):
                entities.append({
                    "text": match.group(),
                    "type": entity_type,
                    "start": match.start(),
                    "end": match.end(),
                })

        # 位置でソート
        entities.sort(key=lambda x: x["start"])
        return entities

    # テスト
    test_texts = [
        "张三は Google で働いており、携帯番号は 13800138000 です。",
        "王小明は北京のマイクロソフト社へ出張する",
        "有问题联系 [email protected]",
        "サンプル はとても良い学習ウェブサイトです。",
    ]

    for text in test_texts:
        entities = extract_entities(text)
        print(f"テキスト:{text}")
        if entities:
            print("認識されたエンティティ:")
            for ent in entities:
                print(f"  - {ent['text']}: {ent['type']}")
        else:
            print("エンティティが認識されませんでした")
        print()

# ============================================
# 大規模モデルでNERを行う
# ============================================

def ner_with_llm():
    """大規模モデルでNERを行うアプローチのデモ"""
    print("大規模モデルでNER(アプローチのデモ)")
    print("-" * 50)

    def extract_entities_with_llm(text: str):
        """LLMでNERを行う(シミュレーション)"""
        # 実際のシナリオ:プロンプトを構築してLLMを呼び出す
        # prompt = f"""以下のテキストから固有表現を抽出してください。
        # JSON形式でのみ返してください。エンティティタイプ:PERSON(人名)、ORG(組織)、LOC(場所)を含みます。
        # テキスト:{text}"""

        # ここでは簡単なシミュレーションを行う
        entities = []

        # 簡単なキーワードマッチングでシミュレート
        if "张三" in text:
            entities.append({"text": "张三", "type": "PERSON"})
        if "Google" in text:
            entities.append({"text": "Google", "type": "ORG"})
        if "北京" in text:
            entities.append({"text": "北京", "type": "LOC"})
        if "サンプル" in text:
            entities.append({"text": "サンプル", "type": "ORG"})

        return entities

    test_text = 张三は北京のGoogle社で働いており、彼はよくサンプルで学習しています。
    entities = extract_entities_with_llm(test_text)

    print(f"テキスト:{test_text}")
    print("認識されたエンティティ:")
    for ent in entities:
        print(f"  - {ent['text']}: {ent['type']}")

# ============================================
# デモを実行
# ============================================

if __name__ == "__main__":
    simple_chinese_ner()
    print("=" * 50)
    ner_with_llm()

機械翻訳

機械翻訳はNLPの最も古典的な応用の一つであり、最も早く商業化された応用でもあります。

ニューラル機械翻訳の原理

初期の機械翻訳はルールベースで、その後は統計ベース、現在はすべてニューラルベースです。

ニューラル機械翻訳(Neural Machine Translation、NMT)は通常、Encoder-Decoderアーキテクチャです:

1. Encoderが原文の文を読み取り、意味表現を生成する

2. Decoderが意味表現に基づいて目的言語の文を生成する

Attentionメカニズムにより、デコーダは各単語を生成する際に原文の文の異なる位置に「注目」できます。これが翻訳品質向上の鍵です。

評価指標:BLEU Score

翻訳品質をどう測るか?BLEU(Bilingual Evaluation Understudy)が最も一般的な指標です。

BLEUの核心的な考え方は:機械翻訳結果と人手による翻訳結果のn-gramの一致度が高いほど、品質が良い。

BLEUスコアの範囲は0から1で、高いほど良いです。一般的に:

0.1以下:ほぼ通じない

0.1-0.3:大体の意味が分かる

0.3-0.5:翻訳品質が良い

0.5以上:人手による翻訳に近い

例

# ============================================
# BLEUスコア計算のデモ
# ============================================

def bleu_score_demo():
    """BLEUスコアの計算のデモ"""
    print("BLEUスコアデモ")
    print("-" * 50)

    # 注意:実際に使用する際はsacrebleuライブラリの使用を推奨
    # ここでは概念のデモを行う

    from collections import Counter
    import math

    def compute_ngrams(tokens, n):
        """n-gramを計算"""
        return [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]

    def simple_bleu(reference: str, candidate: str, max_n: int = 4):
        """簡易BLEU計算(概念デモ)"""
        ref_tokens = reference.split()
        cand_tokens = candidate.split()

        # brevity penalty(短文ペナルティ)を計算
        ref_len = len(ref_tokens)
        cand_len = len(cand_tokens)

        if cand_len == 0:
            return 0.0

        if cand_len <= ref_len:
            bp = math.exp(1 - ref_len / cand_len)
        else:
            bp = 1.0

        # 各n-gramの適合率を計算
        precisions = []
        for n in range(1, max_n + 1):
            ref_ngrams = Counter(compute_ngrams(ref_tokens, n))
            cand_ngrams = Counter(compute_ngrams(cand_tokens, n))

            if not cand_ngrams:
                precisions.append(0.0)
                continue

            # マッチした数をカウント
            matches = 0
            for ngram, count in cand_ngrams.items():
                matches += min(count, ref_ngrams.get(ngram, 0))

            total = sum(cand_ngrams.values())
            precisions.append(matches / total if total > 0 else 0.0)

        # 幾何平均
        if all(p == 0 for p in precisions):
            geo_mean = 0.0
        else:
            # log(0)を回避
            log_sum = sum(math.log(p + 1e-10) for p in precisions) / max_n
            geo_mean = math.exp(log_sum)

        bleu = bp * geo_mean
        return bleu

    # テスト
    reference = "the cat sat on the mat"
    candidates = [
        "the cat sat on the mat",  # 完全一致
        "the cat was on the mat",  # 1語だけ異なる
        "the cat sat on a mat",    # 冠詞が異なる
        "a cat sat on the mat",
        "the dog sat on the mat",  # 1語が誤り
        "mat the on sat cat the",  # 順序が完全に乱れている
        "hello world",             # 完全に関連しない
    ]

    print(f"参考翻訳:{reference}")
    print()
    for candidate in candidates:
        bleu = simple_bleu(reference, candidate)
        print(f"候補翻訳:{candidate}")
        print(f"BLEUスコア:{bleu:.4f}")
        print()

    # 中国語の例
    print("中国語翻訳の例:")
    print("-" * 30)
    ref = "我 喜欢 吃 苹果"
    cand1 = "我 喜欢 吃 苹果"
    cand2 = "我 爱 吃 苹果"
    cand3 = "苹果 我 喜欢 吃"

    print(f"参考:{ref}")
    print(f"候補1:{cand1}, BLEU: {simple_bleu(ref, cand1):.4f}")
    print(f"候補2:{cand2}, BLEU: {simple_bleu(ref, cand2):.4f}")
    print(f候補3:{cand3}, BLEU: {simple_bleu(ref, cand3):.4f})


if __name__ == "__main__":
    bleu_score_demo()

LLM 時代の翻訳品質

大規模モデルの登場後、機械翻訳の品質はさらに一段階向上した。

従来の翻訳モデルはバイリンガルの平行データで訓練されていたが、大規模モデルは膨大なテキストで訓練され、言語の理解がより深い。

大規模モデルの翻訳における利点:

1. 文脈理解が優れている:曖昧さや多義語を処理できる

2. スタイル制御:翻訳のトーンやスタイルを指定できる

3. 用語の一貫性:用語集を提供することで、固有名詞の翻訳を一貫させられる

4. 多言語対応:1つのモデルで数十〜数百言語の相互翻訳ができる


感情分析

感情分析とは、テキストの感情的な傾向(肯定的か、否定的か、それとも中立的か)を判断することである。

細粒度感情分析

単純な感情分析は2値分類(肯定的/否定的)だが、より細かい粒度のものは次のようになる:

1. 感情スコアリング:1〜5の星評価で、1が最も否定的、5が最も肯定的

2. 感情分類:怒り、喜び、悲しみ、驚きなど

3. アスペクトベース感情分析(Aspect-Based Sentiment Analysis):全体的な感情だけでなく、特定の側面に対する感情も分析する。

例:「このレストランは料理はとても美味しいが、サービスが遅すぎる」

全体的な感情:中立よりやや肯定的

アスペクトベースの感情:

料理 → 肯定的

サービス → 否定的

多言語感情モデル

現在の感情モデルは複数の言語を処理でき、言語ごとに個別に訓練する必要がない。

よく使われる多言語モデル:

XLM-RoBERTa、mBERT(多言語BERT)、Llama、Claudeなどの大規模モデル。


テキスト要約

テキスト要約とは、長いテキストを短くし、核となる情報を保持することである。

抽出型 vs 生成型要約

主な要約方法は2つある:

抽出型要約:原文から最も重要な文を選んでつなぎ合わせる。

長所:情報が正確で、捏造がない

短所:流暢さに欠け、冗長な情報が含まれる可能性がある

生成型要約:モデルに原文を「理解」させ、その後自分の言葉で要約を書かせる。

長所:流暢で簡潔、原文を要約できる

短所:「幻覚」(ハルシネーション)を起こし、原文にない情報を捏造する可能性がある

大規模モデルの時代では、生成型要約が主流になっているが、使用時には幻覚(ハルシネーション)の問題に注意する必要がある。

ROUGE 評価指標

要約の品質はどう評価するのか?ROUGEが最もよく使われる指標である。

ROUGEにはいくつかの変種がある:

ROUGE-1:ユニグラム(単語1つ)の一致に基づく

ROUGE-2:バイグラム(2語)の一致に基づく

ROUGE-L:最長共通部分列に基づく

BLEUと同様に、ROUGEも機械が生成した要約と人手による参考要約の重なり具合を評価する。

例

# ============================================
# テキスト要約:ROUGE指標 + 簡単な抽出型要約
# ============================================

def rouge_demo():
    """ROUGE指標のデモ"""
    print("ROUGE指標のデモ")
    print("-" * 50)

    from collections import Counter

    def compute_rouge_1(reference: str, candidate: str) -> float:
        """ROUGE-1(ユニグラムF1)の計算"""
        ref_tokens = reference.split()
        cand_tokens = candidate.split()

        if not ref_tokens or not cand_tokens:
            return 0.0

        ref_counts = Counter(ref_tokens)
        cand_counts = Counter(cand_tokens)

        # 一致数を計算する
        matches = 0
        for token, count in cand_counts.items():
            matches += min(count, ref_counts.get(token, 0))

        precision = matches / len(cand_tokens)
        recall = matches / len(ref_tokens)

        if precision + recall == 0:
            return 0.0

        f1 = 2 * precision * recall / (precision + recall)
        return f1

    def compute_lcs(a: list, b: list) -> int:
        """最長共通部分列の長さを計算する"""
        m, n = len(a), len(b)
        dp = [[0] * (n + 1) for _ in range(m + 1)]

        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if a[i-1] == b[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

        return dp[m][n]

    def compute_rouge_l(reference: str, candidate: str) -> float:
        """ROUGE-L(最長共通部分列F1)の計算"""
        ref_tokens = reference.split()
        cand_tokens = candidate.split()

        if not ref_tokens or not cand_tokens:
            return 0.0

        lcs_len = compute_lcs(ref_tokens, cand_tokens)

        precision = lcs_len / len(cand_tokens)
        recall = lcs_len / len(ref_tokens)

        if precision + recall == 0:
            return 0.0

        f1 = 2 * precision * recall / (precision + recall)
        return f1

    # テスト
    reference = "自然言語処理は人工知能の重要な分野であり、コンピュータと人間の言語のインタラクションを研究する。"
    candidates = [
        "自然言語処理は人工知能の重要な分野であり、コンピュータと人間の言語のインタラクションを研究する。",  # 完全一致
        "自然言語処理はコンピュータと人間の言語のインタラクションを研究し、人工知能の重要な分野である。",  # 順序入れ替え
        "自然言語処理は人工知能の分野であり、コンピュータと言語のインタラクションを研究する。",  # 削減版
        "サンプルはプログラミング学習サイトである。",  # 無関係
    ]

    print(f"参考要約:{reference}")
    print()
    for candidate in candidates:
        rouge1 = compute_rouge_1(reference, candidate)
        rougel = compute_rouge_l(reference, candidate)
        print(f"候補要約:{candidate}")
        print(f"ROUGE-1: {rouge1:.4f}")
        print(f"ROUGE-L: {rougel:.4f}")
        print()


def simple_extractive_summarization():
    """簡単な抽出型要約:単語頻度に基づいて重要な文を選ぶ"""
    print("簡単な抽出型要約のデモ")
    print("-" * 50)

    import re
    from collections import Counter

    def split_sentences(text: str) -> list:
        """簡単な中国語の文分割"""
        # 句点、感嘆符、疑問符で文を分割
        sentences = re.split(r'[。!?]', text)
        # 空文字列を除去
        sentences = [s.strip() for s in sentences if s.strip()]
        return sentences

    def get_word_frequency(text: str) -> Counter:
        """単語頻度の計算(簡単なデモ:文字レベルのn-gramで模擬)"""
        # 実際の場面では、jiebaなどの分かち書きツールを使うべき
        # ここでは簡単にユニグラムを使う
        chars = [c for c in text if c.strip()]
        return Counter(chars)

    def sentence_score(sentence: str, word_freq: Counter) -> float:
        """文のスコアを計算する"""
        if not sentence:
            return 0.0

        # 簡単なスコア:文中の単語の頻度の合計 / 文の長さ
        total_score = 0.0
        for char in sentence:
            total_score += word_freq.get(char, 0)

        # 正規化
        return total_score / (len(sentence) + 1)

    def extractive_summary(text: str, num_sentences: int = 2) -> str:
        """抽出型要約"""
        sentences = split_sentences(text)

        if len(sentences) <= num_sentences:
            return text

        # 単語頻度を計算
        word_freq = get_word_frequency(text)

        # 各文のスコアを計算
        scores = [(i, sentence_score(s, word_freq)) for i, s in enumerate(sentences)]

        # スコアでソートし、上位のものを選ぶ
        scores.sort(key=lambda x: x[1], reverse=True)
        top_indices = [i for i, score in scores[:num_sentences]]
        top_indices.sort()  # 原文の順序を保持

        # 結果を連結
        summary = "。".join(sentences[i] for i in top_indices) + "。"
        return summary

    # テストテキスト
    test_text = """
自然言語処理は人工知能の重要な分野である。これは、コンピュータが人間の言語を理解し生成する方法を研究するものである。
この技術には、機械翻訳、感情分析、質問応答システムなど、多くの応用がある。
近年、大規模言語モデルの登場により、自然言語処理は大きな進展を遂げた。
サンプルサイトは多くの優れたプログラミングチュートリアルを提供し、開発者が新しいスキルを学ぶのを支援している。
"""


    print("原文:")
    print(test_text.strip())
    print()

    summary = extractive_summary(test_text, num_sentences=2)
    print("抽出型要約:")
    print(summary)


if __name__ == "__main__":
    rouge_demo()
    print("=" * 50)
    simple_extractive_summarization()
その他の拡張