!date
Tue Jun 23 09:31:12 AM UTC 2026
32. 演習:ドメインの異なるコーパスで言語モデルを比較する#
32.1. 概要#
文学(夏目漱石「坊っちゃん」)とスポーツニュース(livedoor ニュースコーパス)という 異なるドメインのコーパスから トライグラム言語モデル(Laplace スムージング) を構築し、 確率分布・補完候補・Perplexity がどう変わるかを実測します。
ドメイン |
テキスト |
出典 |
ライセンス |
|---|---|---|---|
文学 |
夏目漱石「坊っちゃん」(1906年) |
パブリックドメイン |
|
スポーツニュース |
livedoor ニュースコーパス Sports Watch |
CC BY-ND 2.1 JP |
実行環境:Google Colab 無料枠(CPU)で動作します。GPU は不要です。
# ライブラリのインストール(初回のみ、数分かかります)
!pip install spacy plotly --quiet
!python -m spacy download ja_core_news_sm --quiet
print("インストール完了")
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.1/12.1 MB 86.9 MB/s eta 0:00:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72.2/72.2 MB 11.3 MB/s eta 0:00:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 72.3 MB/s eta 0:00:00
?25h✔ Download and installation successful
You can now load the package via spacy.load('ja_core_news_sm')
⚠ Restart to reload dependencies
If you are in a Jupyter or Colab notebook, you may need to restart Python in
order to load all the package's dependencies. You can do this by selecting the
'Restart kernel' or 'Restart runtime' option.
インストール完了
import re, math, zipfile, tarfile, urllib.request
from collections import Counter, defaultdict
import spacy
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
print("インポート完了")
インポート完了
32.2. 1. コーパスのダウンロードと前処理#
2 つの異なるドメインのコーパスを公開データから直接取得します。
# 青空文庫「坊っちゃん」をダウンロード(ShiftJIS / zip)
AOZORA_URL = "https://www.aozora.gr.jp/cards/000148/files/752_ruby_2438.zip"
urllib.request.urlretrieve(AOZORA_URL, "bocchan.zip")
with zipfile.ZipFile("bocchan.zip") as zf:
fname = [n for n in zf.namelist() if n.endswith(".txt")][0]
raw = zf.read(fname).decode("shift_jis", errors="replace")
def clean_aozora(text):
"""青空文庫のルビ・注記・ヘッダ・フッタを除去する"""
text = re.sub(r'^.*?-{5,}[^\n]*\n', '', text, flags=re.DOTALL)
text = re.split(r'\n底本:', text)[0]
text = re.sub(r'《[^》]*》', '', text)
text = re.sub(r'[|]', '', text)
text = re.sub(r'[#[^]]*]', '', text)
text = re.sub(r'\n{3,}', '\n\n', text)
return text.strip()
bocchan_text = clean_aozora(raw)
print(f"坊っちゃん: {len(bocchan_text):,} 文字")
print("冒頭サンプル:", bocchan_text[:80])
坊っちゃん: 89,670 文字
冒頭サンプル: 【テキスト中に現れる記号について】
:ルビ
(例)坊っちゃん
:ルビの付く文字列の始まりを特定する記号
(例)夕方折戸の
:入力者注 主に
# livedoor ニュースコーパス(Sports Watch カテゴリ)をダウンロード(約 20MB)
LIVEDOOR_URL = "https://www.rondhuit.com/download/ldcc-20140209.tar.gz"
print("ダウンロード中(しばらくお待ちください)...")
urllib.request.urlretrieve(LIVEDOOR_URL, "ldcc.tar.gz")
sports_texts = []
with tarfile.open("ldcc.tar.gz", "r:gz") as tf:
for member in tf.getmembers():
if "sports-watch" in member.name and member.name.endswith(".txt"):
f = tf.extractfile(member)
if f is None:
continue
content = f.read().decode("utf-8", errors="replace")
lines = content.strip().split("\n")
# 先頭 2 行(URL・日時)を除いて本文のみ取得
body = "\n".join(lines[2:]) if len(lines) > 2 else content
if len(body.strip()) > 30:
sports_texts.append(body.strip())
sports_text = "\n".join(sports_texts)
print(f"スポーツニュース: {len(sports_texts)} 記事, {len(sports_text):,} 文字")
print("先頭サンプル:", sports_text[:80])
ダウンロード中(しばらくお待ちください)...
スポーツニュース: 901 記事, 620,431 文字
先頭サンプル: 原著作者のクレジットを表示し、ニュース記事の改変をしないことを条件に、記事全文を
自由に転載・引用が可能です。
このディレクトリの記事ファイル内容の提供元:
32.3. 2. spaCy でトークナイズ(形態素解析)#
日本語テキストを単語(形態素)に分割します。
spaCy の ja_core_news_sm モデルを使用します。
空白・句読点は除外します
速度のため各コーパスは先頭 60,000 文字を使用します
# spaCy モデルをロード
nlp = spacy.load("ja_core_news_sm")
# Sudachi(spaCy 日本語モデルの内部トークナイザ)の入力上限は約 49,149 bytes。
# 日本語 1 文字は UTF-8 で最大 3 bytes なので、1 チャンクあたり 12,000 文字に抑える。
CHUNK_SIZE = 12_000
EXCLUDE_CHARS = set("。、!?「」『』()…―・\n\t ")
def tokenize(text, max_chars=60_000):
"""テキストを形態素(表層形)のリストに変換する。
Sudachi の入力サイズ制限に対応するため CHUNK_SIZE 文字ずつ処理する。"""
text = text[:max_chars]
tokens = []
for start in range(0, len(text), CHUNK_SIZE):
chunk = text[start : start + CHUNK_SIZE]
doc = nlp(chunk)
tokens.extend(
t.text for t in doc
if not t.is_space
and not t.is_punct
and t.text.strip()
and t.text not in EXCLUDE_CHARS
)
return tokens
print("トークナイズ中(30 秒〜1 分程度かかります)...")
tokens_b = tokenize(bocchan_text)
tokens_s = tokenize(sports_text)
print(f"\n坊っちゃん : {len(tokens_b):,} トークン 語彙 {len(set(tokens_b)):,} 語")
print(f"スポーツ : {len(tokens_s):,} トークン 語彙 {len(set(tokens_s)):,} 語")
print("\n先頭 20 トークン(坊っちゃん):", tokens_b[:20])
print("先頭 20 トークン(スポーツ) :", tokens_s[:20])
トークナイズ中(30 秒〜1 分程度かかります)...
坊っちゃん : 34,909 トークン 語彙 4,096 語
スポーツ : 30,022 トークン 語彙 4,569 語
先頭 20 トークン(坊っちゃん): ['テキスト', '中', 'に', '現れる', '記号', 'に', 'つい', 'て', 'ルビ', '例', '坊っ', 'ちゃん', 'ルビ', 'の', '付く', '文字', '列', 'の', '始まり', 'を']
先頭 20 トークン(スポーツ) : ['原', '著作', '者', 'の', 'クレジット', 'を', '表示', 'し', 'ニュース', '記事', 'の', '改変', 'を', 'し', 'ない', 'こと', 'を', '条件', 'に', '記事']
32.4. 3. トライグラム言語モデルの構築(Laplace スムージング)#
カウントがゼロの n-gram にも小さい確率を割り当て、ゼロ確率問題を回避します。
def build_trigram_lm(tokens):
"""トークン列からトライグラム LM を構築する。
Returns:
bi_counts : Counter -- C(w1, w2) バイグラムカウント(分母)
tri_counts : defaultdict -- C(w3 | w1, w2) トライグラムカウント(分子)
vocab : set -- 語彙
"""
bi_counts = Counter()
tri_counts = defaultdict(Counter)
vocab = set(tokens)
for i in range(2, len(tokens)):
w1, w2, w3 = tokens[i-2], tokens[i-1], tokens[i]
bi_counts[(w1, w2)] += 1
tri_counts[(w1, w2)][w3] += 1
return bi_counts, tri_counts, vocab
def trigram_prob(w1, w2, w3, bi_counts, tri_counts, vocab_size, k=1):
"""Laplace スムージング付き P(w3 | w1, w2) を返す"""
c_tri = tri_counts[(w1, w2)][w3]
c_bi = bi_counts[(w1, w2)]
return (c_tri + k) / (c_bi + k * vocab_size)
# 両コーパスのモデルを構築
bi_b, tri_b, vocab_b = build_trigram_lm(tokens_b)
bi_s, tri_s, vocab_s = build_trigram_lm(tokens_s)
VS = len(vocab_b | vocab_s) # クロスドメイン評価用の合算語彙サイズ
print(f"文学LM : バイグラム文脈 {len(bi_b):,} 種 トライグラム {sum(len(v) for v in tri_b.values()):,} 種")
print(f"ニュースLM: バイグラム文脈 {len(bi_s):,} 種 トライグラム {sum(len(v) for v in tri_s.values()):,} 種")
print(f"合算語彙サイズ |V| = {VS:,}")
文学LM : バイグラム文脈 17,576 種 トライグラム 28,609 種
ニュースLM: バイグラム文脈 16,874 種 トライグラム 24,929 種
合算語彙サイズ |V| = 7,608
32.5. 4. 確率分布の比較#
32.5.1. 4-1 頻出語(単語出現回数)#
2 つのドメインで頻出する語がどう違うかを可視化します。
TOP_N = 20
cnt_b = Counter(tokens_b)
cnt_s = Counter(tokens_s)
top_b = cnt_b.most_common(TOP_N)
top_s = cnt_s.most_common(TOP_N)
fig = make_subplots(
rows=1, cols=2,
subplot_titles=["坊っちゃん(文学)", "Sports Watch(ニュース)"]
)
fig.add_trace(
go.Bar(x=[w for w, _ in top_b], y=[c for _, c in top_b],
name="文学", marker_color="#4682B4"),
row=1, col=1
)
fig.add_trace(
go.Bar(x=[w for w, _ in top_s], y=[c for _, c in top_s],
name="ニュース", marker_color="#CD5C5C"),
row=1, col=2
)
fig.update_layout(title_text="頻出語 Top-20 比較", showlegend=False, height=420)
fig.show()
print("【坊っちゃん Top-10】:", [w for w, _ in top_b[:10]])
print("【スポーツ Top-10】:", [w for w, _ in top_s[:10]])
【坊っちゃん Top-10】: ['て', 'の', 'に', 'が', 'た', 'は', 'と', 'を', 'だ', 'で']
【スポーツ Top-10】: ['の', 'と', 'た', 'に', 'は', 'を', 'が', 'て', 'で', 'し']
32.5.2. 4-2 頻出トライグラム#
各コーパスで最も多く出現したトライグラム(3 語の連続)を確認します。
def top_trigrams(tri_counts, n=15):
flat = Counter()
for (w1, w2), nexts in tri_counts.items():
for w3, cnt in nexts.items():
flat[(w1, w2, w3)] = cnt
return flat.most_common(n)
tri_top_b = top_trigrams(tri_b, 15)
tri_top_s = top_trigrams(tri_s, 15)
fig = go.Figure(data=[go.Table(
columnwidth=[50, 260, 50, 260],
header=dict(
values=["順位", "坊っちゃん 上位トライグラム", "順位", "スポーツ 上位トライグラム"],
fill_color="#CCCCCC",
align="center",
font=dict(size=13)
),
cells=dict(
values=[
list(range(1, 16)),
[f"「{w1}」→「{w2}」→「{w3}」 ({cnt}回)" for (w1, w2, w3), cnt in tri_top_b],
list(range(1, 16)),
[f"「{w1}」→「{w2}」→「{w3}」 ({cnt}回)" for (w1, w2, w3), cnt in tri_top_s],
],
align=["center", "left", "center", "left"],
font=dict(size=12),
height=28,
)
)])
fig.update_layout(title="頻出トライグラム Top-15 比較", height=520)
fig.show()
32.5.3. 4-3 同じ 2 語文脈での次語分布#
同じ 2 語を文脈として与えたとき、2 つのモデルが次の語をどう予測するかを比較します。 両コーパスに共通して出現する文脈を自動選択します。
def next_word_dist(w1, w2, tri_counts, bi_counts, vocab_size, top_k=8, k=1):
"""(w1, w2) を文脈とした次語の Top-K と Laplace 確率を返す"""
cands = {w3: (cnt + k) / (bi_counts[(w1, w2)] + k * vocab_size)
for w3, cnt in tri_counts[(w1, w2)].items()}
if not cands:
return []
return sorted(cands.items(), key=lambda x: x[1], reverse=True)[:top_k]
# 両コーパスに共通して頻出する 2 語文脈を自動選択
shared_bi = set(bi_b.keys()) & set(bi_s.keys())
good_contexts = sorted(
[ctx for ctx in shared_bi
if bi_b[ctx] >= 5 and bi_s[ctx] >= 5 # 両方で 5 回以上
and len(tri_b[ctx]) >= 3 and len(tri_s[ctx]) >= 3], # 次語の種類が 3 以上
key=lambda x: bi_b[x] + bi_s[x],
reverse=True
)[:4]
print("比較に使う 2 語文脈(頻度順):", good_contexts)
for (w1, w2) in good_contexts:
dist_b = next_word_dist(w1, w2, tri_b, bi_b, VS)
dist_s = next_word_dist(w1, w2, tri_s, bi_s, VS)
all_words = list(dict.fromkeys([w for w, _ in dist_b[:6]] + [w for w, _ in dist_s[:6]]))
probs_b = {w: p for w, p in dist_b}
probs_s = {w: p for w, p in dist_s}
fig = go.Figure()
fig.add_trace(go.Bar(
name="文学LM", x=all_words,
y=[probs_b.get(w, 0) for w in all_words],
marker_color="#4682B4"
))
fig.add_trace(go.Bar(
name="ニュースLM", x=all_words,
y=[probs_s.get(w, 0) for w in all_words],
marker_color="#CD5C5C"
))
fig.update_layout(
title=f"文脈「{w1}」→「{w2}」 次語の確率分布",
xaxis_title="次語", yaxis_title="P(次語 | 文脈)",
barmode="group", height=380
)
fig.show()
比較に使う 2 語文脈(頻度順): [('し', 'て'), ('し', 'た'), ('て', 'いる'), ('に', 'は')]
32.6. 5. テキスト補完の比較(トライグラムベース)#
2 語の文脈から次の語を予測します(スマホ補完の原理)。
ドメイン固有の文脈:片方のコーパスにしか出現しない文脈
共通の文脈:両コーパスに出現するが、後続語の分布が異なる文脈
def complete(w1, w2, tri_counts, bi_counts, vocab_size, top_k=5, k=1):
"""2 語文脈から次の語 Top-K と Laplace 確率を返す"""
cands = {w3: (cnt + k) / (bi_counts[(w1, w2)] + k * vocab_size)
for w3, cnt in tri_counts[(w1, w2)].items()}
if not cands:
return [("(この文脈のデータなし)", 0.0)]
return sorted(cands.items(), key=lambda x: x[1], reverse=True)[:top_k]
# ① 文学コーパス固有の文脈
lit_only = sorted(
[ctx for ctx in bi_b
if bi_b[ctx] >= 3 and bi_s.get(ctx, 0) == 0 and len(tri_b[ctx]) >= 2],
key=lambda x: bi_b[x], reverse=True
)[:3]
# ② スポーツコーパス固有の文脈
sport_only = sorted(
[ctx for ctx in bi_s
if bi_s[ctx] >= 3 and bi_b.get(ctx, 0) == 0 and len(tri_s[ctx]) >= 2],
key=lambda x: bi_s[x], reverse=True
)[:3]
# ③ 共通文脈(次語の分布が異なるはず)
shared_ex = sorted(
[ctx for ctx in shared_bi
if bi_b[ctx] >= 3 and bi_s[ctx] >= 3
and len(tri_b[ctx]) >= 2 and len(tri_s[ctx]) >= 2],
key=lambda x: bi_b[x] + bi_s[x], reverse=True
)[:3]
for section, contexts, model_pair in [
("① 文学固有の文脈", lit_only, (tri_b, bi_b, tri_s, bi_s)),
("② スポーツ固有の文脈", sport_only, (tri_b, bi_b, tri_s, bi_s)),
("③ 共通文脈(違いに注目)", shared_ex, (tri_b, bi_b, tri_s, bi_s)),
]:
print(f"\n=== {section} ===")
tb, bb, ts, bs = model_pair
for (w1, w2) in contexts:
r_lit = complete(w1, w2, tb, bb, VS)
r_news = complete(w1, w2, ts, bs, VS)
print(f" 文脈「{w1}」「{w2}」")
print(f" 文学LM : {[w for w, _ in r_lit]}")
print(f" ニュースLM: {[w for w, _ in r_news]}")
=== ① 文学固有の文脈 ===
文脈「おれ」「は」
文学LM : ['こう', '何', 'この', '無論', '一']
ニュースLM: ['(この文脈のデータなし)']
文脈「赤」「シャツ」
文学LM : ['は', 'が', 'の', 'と', 'さん']
ニュースLM: ['(この文脈のデータなし)']
文脈「と」「云っ」
文学LM : ['た', 'て', 'たら', 'てる', 'たって']
ニュースLM: ['(この文脈のデータなし)']
=== ② スポーツ固有の文脈 ===
文脈「Sports」「Watch」
文学LM : ['(この文脈のデータなし)']
ニュースLM: ['日本', '競馬', '浅田', 'TBS', '岡田']
文脈「日本」「代表」
文学LM : ['(この文脈のデータなし)']
ニュースLM: ['は', 'の', 'に', 'メンバー', 'が']
文脈「です」「けど」
文学LM : ['(この文脈のデータなし)']
ニュースLM: ['と', 'も', '90', 'ああ', '浅田']
=== ③ 共通文脈(違いに注目) ===
文脈「し」「て」
文学LM : ['いる', 'い', 'も', 'くれ', 'くれる']
ニュースLM: ['いる', 'い', 'も', '知ら', 'いく']
文脈「し」「た」
文学LM : ['事', 'と', 'が', 'する', 'から']
ニュースLM: ['また', 'が', 'Sports', 'こと', 'と']
文脈「て」「いる」
文学LM : ['おれ', 'と', 'の', 'ん', 'から']
ニュースLM: ['の', 'と', 'ん', 'よう', 'から']
32.7. 6. クロスドメイン Perplexity 評価#
仮説:自分が学習したドメインのテキストに対して PPL は低く、他ドメインに対して高くなる。
組み合わせ |
期待される PPL |
|---|---|
文学LM × 文学テスト |
低い |
文学LM × ニューステスト |
高い |
ニュースLM × 文学テスト |
高い |
ニュースLM × ニューステスト |
低い |
def sentence_logprob(tokens, bi_counts, tri_counts, vocab_size, k=1):
"""トークン列の log P(トライグラム・Laplace スムージング)
位置 2 以降を評価対象とする。短すぎる場合は (None, 0) を返す。"""
if len(tokens) < 3:
return None, 0
lp, n = 0.0, 0
for i in range(2, len(tokens)):
p = trigram_prob(tokens[i-2], tokens[i-1], tokens[i],
bi_counts, tri_counts, vocab_size, k)
lp += math.log(p)
n += 1
return lp, n
def perplexity(token_sents, bi_counts, tri_counts, vocab_size, k=1):
"""文リスト全体のトライグラム Perplexity"""
total_lp, total_n = 0.0, 0
for sent in token_sents:
lp, n = sentence_logprob(sent, bi_counts, tri_counts, vocab_size, k)
if lp is not None and n > 0:
total_lp += lp
total_n += n
return math.exp(-total_lp / total_n) if total_n > 0 else float("inf")
# テスト文を準備(句点で分割し先頭 200 文をトークナイズ)
def make_test_sents(text, max_sents=200, max_chars_per_sent=500):
"""テキストを文に分割してトークナイズしたリストを返す"""
sents = [s.strip() for s in re.split(r'[。\n]', text) if len(s.strip()) > 5]
sents = sents[:max_sents]
result = []
for s in sents:
toks = tokenize(s, max_chars=max_chars_per_sent)
if len(toks) >= 3:
result.append(toks)
return result
print("テスト文を準備中...")
print("文学テスト:")
test_b = make_test_sents(bocchan_text)
print(f" → {len(test_b)} 文")
print("ニューステスト:")
test_s = make_test_sents(sports_text)
print(f" → {len(test_s)} 文")
テスト文を準備中...
文学テスト:
→ 199 文
ニューステスト:
→ 199 文
print("Perplexity 計算中...")
ppl = {
("文学LM", "文学テスト"): perplexity(test_b, bi_b, tri_b, VS),
("文学LM", "ニューステスト"): perplexity(test_s, bi_b, tri_b, VS),
("ニュースLM","文学テスト"): perplexity(test_b, bi_s, tri_s, VS),
("ニュースLM","ニューステスト"): perplexity(test_s, bi_s, tri_s, VS),
}
print("\n=== クロスドメイン Perplexity ===")
print(f"{'モデル':^12} × {'テストデータ':^14} → {'PPL':>8} 評価")
print("-" * 55)
for (model, test), v in ppl.items():
same = "◎ 同ドメイン" if (model.startswith("文学") and test.startswith("文学")) \
or (model.startswith("ニュース") and test.startswith("ニュース")) else "△ 異ドメイン"
print(f"{model:^12} × {test:^14} → {v:>8.1f} {same}")
Perplexity 計算中...
=== クロスドメイン Perplexity ===
モデル × テストデータ → PPL 評価
-------------------------------------------------------
文学LM × 文学テスト → 2861.8 ◎ 同ドメイン
文学LM × ニューステスト → 6825.2 △ 異ドメイン
ニュースLM × 文学テスト → 6612.8 △ 異ドメイン
ニュースLM × ニューステスト → 3064.0 ◎ 同ドメイン
ppl_bb = ppl[("文学LM", "文学テスト")]
ppl_bs = ppl[("文学LM", "ニューステスト")]
ppl_sb = ppl[("ニュースLM","文学テスト")]
ppl_ss = ppl[("ニュースLM","ニューステスト")]
z = [[ppl_bb, ppl_bs], [ppl_sb, ppl_ss]]
x_labels = ["文学テスト", "ニューステスト"]
y_labels = ["文学LM", "ニュースLM"]
fig = go.Figure(data=go.Heatmap(
z=z, x=x_labels, y=y_labels,
colorscale="YlOrRd", reversescale=False,
text=[[f"{ppl_bb:.0f}", f"{ppl_bs:.0f}"],
[f"{ppl_sb:.0f}", f"{ppl_ss:.0f}"]],
texttemplate="%{text}",
textfont=dict(size=20),
hovertemplate="モデル: %{y}<br>テスト: %{x}<br>PPL = %{z:.1f}<extra></extra>",
))
fig.update_layout(
title=dict(
text="クロスドメイン Perplexity ヒートマップ<br>"
"<sup>値が低いほど良い(低 PPL = より自然に次語を予測できる)</sup>",
font=dict(size=14)
),
xaxis_title="テストデータ",
yaxis_title="モデル",
height=420, width=520,
)
fig.show()
print("\n読み方:対角線(同ドメイン)の PPL が低く、")
print("非対角線(異ドメイン)の PPL が高いほどドメインの差が明確に表れています。")
読み方:対角線(同ドメイン)の PPL が低く、
非対角線(異ドメイン)の PPL が高いほどドメインの差が明確に表れています。
32.8. 7. 語彙重複分析とドメイン特徴語#
2 つのコーパスの語彙がどの程度重なるかを分析し、 各ドメイン固有の語彙(ドメイン特徴語)を抽出します。
vocab_b_only = vocab_b - vocab_s
vocab_s_only = vocab_s - vocab_b
vocab_shared = vocab_b & vocab_s
print("=== 語彙重複分析 ===")
print(f"坊っちゃん 語彙: {len(vocab_b):,} 語")
print(f"スポーツ 語彙: {len(vocab_s):,} 語")
print(f"共通語彙 : {len(vocab_shared):,} 語 "
f"(坊の {len(vocab_shared)/len(vocab_b)*100:.1f}%、"
f"スポの {len(vocab_shared)/len(vocab_s)*100:.1f}%)")
print(f"文学のみ : {len(vocab_b_only):,} 語")
print(f"スポーツのみ : {len(vocab_s_only):,} 語")
fig = make_subplots(
rows=1, cols=2, specs=[[{"type": "pie"}, {"type": "pie"}]],
subplot_titles=["坊っちゃん語彙の構成", "スポーツ語彙の構成"]
)
fig.add_trace(go.Pie(
labels=["共通語彙", "文学のみ"],
values=[len(vocab_shared), len(vocab_b_only)],
marker_colors=["#7EB5D6", "#4682B4"], name="坊っちゃん"
), row=1, col=1)
fig.add_trace(go.Pie(
labels=["共通語彙", "スポーツのみ"],
values=[len(vocab_shared), len(vocab_s_only)],
marker_colors=["#F4A582", "#CD5C5C"], name="スポーツ"
), row=1, col=2)
fig.update_layout(title="語彙の重複率", height=400)
fig.show()
=== 語彙重複分析 ===
坊っちゃん 語彙: 4,096 語
スポーツ 語彙: 4,569 語
共通語彙 : 1,057 語 (坊の 25.8%、スポの 23.1%)
文学のみ : 3,039 語
スポーツのみ : 3,512 語
def domain_keywords(cnt_target, cnt_other, top_n=15):
"""対象コーパスに多く、他コーパスに少ない語を TF 比で抽出する"""
total_t = sum(cnt_target.values())
total_o = sum(cnt_other.values()) + 1
scores = {
w: (cnt / total_t) / (cnt_other.get(w, 0) / total_o + 1e-9)
for w, cnt in cnt_target.items()
}
return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n]
kw_b = domain_keywords(cnt_b, cnt_s)
kw_s = domain_keywords(cnt_s, cnt_b)
print("坊っちゃん固有語(文学ドメイン) :", [w for w, _ in kw_b])
print("スポーツ固有語(ニュースドメイン):", [w for w, _ in kw_s])
fig = make_subplots(
rows=1, cols=2,
subplot_titles=["坊っちゃん固有語(TF 比スコア)", "スポーツ固有語(TF 比スコア)"]
)
fig.add_trace(go.Bar(
x=[w for w, _ in kw_b], y=[s for _, s in kw_b],
marker_color="#4682B4", name="文学"
), row=1, col=1)
fig.add_trace(go.Bar(
x=[w for w, _ in kw_s], y=[s for _, s in kw_s],
marker_color="#CD5C5C", name="スポーツ"
), row=1, col=2)
fig.update_layout(title="ドメイン特徴語(TF 比スコア)", showlegend=False, height=420)
fig.show()
坊っちゃん固有語(文学ドメイン) : ['おれ', '云う', '云っ', '赤', 'シャツ', '山嵐', '清', '野', '生徒', '校長', '男', 'うらなり', '居', '教師', '知れ']
スポーツ固有語(ニュースドメイン): ['Sports', '選手', 'Watch', '1', '代表', '放送', '番組', 'けど', '位', '3', '試合', '監督', '戦', 'W杯', '2']
32.9. 振り返り問題#
Q1:ステップ4の頻出語グラフを見て、2つのドメインの頻出語はどう違いましたか? その違いがトライグラム LM の補完結果にどう影響するかを説明してください。
Q2:ステップ5で「共通文脈③」に対し、文学 LM とニュース LM の補完候補が 異なりました。なぜそうなるかを「トライグラムカウントの差異」で説明してください。
Q3:ステップ6のヒートマップで、対角線(同ドメイン)の PPL が低くなりましたか? この結果を「スマホ補完がユーザー固有のフレーズを学習すると精度が向上する」 という現象と関連づけて説明してください。
Q4:Laplace スムージング(k=1)のとき、未出現トライグラムの確率を式で求め、語彙サイズ |V| = 10000 のときの具体的な値を計算してください。
Q5:バイグラム LM とトライグラム LM を比べると、理論上はどちらが PPL が低い (良い)と期待されますか?またトライグラムには「データスパース問題」の観点から どのようなデメリットがありますか?
!date
Tue Jun 23 09:35:50 AM UTC 2026