論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
単語分割
Word Segmentation
NLP
別称: 分かち書き

🔖 キーワード索引

このページで数える量:分け方の候補(n 文字なら 2n−1 通り)、 単語コストと連接コストの合計、 BPE の隣接ペアの出現回数、 区切り位置の適合率・再現率・F 値。 題材は SSDSE-B-2026 の指標名 109 個(「合計特殊出生率」「延べ宿泊者数」など)で、 Janome(IPAdic 相当)で分けると 526 語になる。

形態素解析MeCabJanomeSudachiPy分かち書き辞書未知語BPEサブワードトークナイザ

別名・略称:分かち書き

💡 30秒で分かる結論

🍰 まずはやさしく

文章を単語ごとに切り分ける作業です。

コンピュータが言葉を理解するために使います。

スマホの予測変換などの仕組みに似ています。

単語分割の基本と代表的な道具を学びます。

単語分割(Word Segmentation):文章を単語に分割する処理

📍 あなたが今見ているもの

🍰 まずはやさしく

文章をバラバラにする最初のステップです。

単語の数を数えるなどの準備のために行います。

「東京都に住んでいる」という文を分けます。

AIがどのように言葉を切るかを見ていきましょう。

日本語の自然言語処理(NLP)では 最初のステップが単語分割。 たとえば 「東京都に住んでいる」 を 「東京 / 都 / に / 住ん / で / いる」 に分割します。 これがないと「単語の頻度を数える」「単語ベクトルを作る」といった後続処理ができません。 ChatGPT などの LLM では サブワード分割(BPE) が使われ、 「東京」→「東/京」 のように細かく区切られることもあります。

🎨 直感で掴む

🍰 まずはやさしく

言葉の切り分け方にはいくつかの種類があります。

状況に合わせて最適な方法を選ぶためです。

部活の報告書をどう分けるか考えるようなものです。

3つの切り分け方の特徴と違いを解説します。

単語分割の3アプローチ

アプローチ代表ツール特徴
辞書+ラティス探索MeCab, Janome速い、 辞書品質に依存
統計モデル(CRF)MeCab+IPADIC, SudachiPy未知語に強い
サブワード(BPE/SentencePiece)SentencePiece, HuggingFace未知語ゼロ、 LLM標準

具体例

  • 原文:「東京都に住んでいる」
  • MeCab:東京 / 都 / に / 住ん / で / いる(6トークン)
  • SentencePiece:▁東京 / 都 / に / 住 / んで / いる(6トークン、 サブワード)
  • 英語tokenize:I'm / living / in / Tokyo

📐 定義 / 数式

🍰 まずはやさしく

最も正しい分け方を確率で決める計算です。

数学的に正しい答えを導き出すために使います。

テストの正解を論理的に導く感覚に似ています。

分割を決めるための数式について詳しく読みます。

単語分割は確率最大の単語列を探す問題として定式化できます。

【最尤分割】
$$W^* = \arg\max_{W} P(W | S) = \arg\max_{W} P(S | W) P(W)$$
S:文、 W:単語列の候補
【BPE のマージ規則】
$$\text{merge}(a, b) \Leftrightarrow (a, b) = \arg\max_{(x,y)} \text{count}(xy)$$
頻度の高いバイトペアを順次マージしてサブワード語彙を構築

🔬 記号・式を言葉で読み解く

辞書
「単語」と「品詞・読み」のセット。 IPADIC, NEologd, UniDic が代表。
ラティス
文の全ての分割候補をグラフ状に列挙したもの。 最短経路探索で最尤分割。
コスト
MeCab では辞書に「単語コスト」「接続コスト」が定義され、 総コスト最小の経路が選ばれる。
BPE
Byte-Pair Encoding。 頻度の高い文字ペアをマージしていく。
未知語
辞書に登録のない語。 統計モデル or サブワードで対処。

🔬 数式を言葉で読み解く(拡張版)

単語分割の核心は 「文字列 $s$ を、 単語列 $w_1 w_2 \cdots w_m$ に最も尤もらしく分けるパス」を求める動的計画問題です。 数式の各記号がどのアルゴリズム段階(辞書引き/コスト計算/Viterbi 探索)に対応するかを丁寧に追うと、 MeCab・Janome・sudachi の挙動の差まで根本から納得できます。 ここでは 4 つの主要記号を 1 つずつ日本語で読み解きます。

$\arg\max_{w_1 \dots w_m} P(w_1 \dots w_m \mid s)$ — 左辺(最尤分割)
「文字列 $s$ を与えたとき、 最も生成確率の高い単語列を探す」が左辺の意味。 $\arg\max$ は「最大値を与える引数(=分割)」を返す演算子。 たとえば「東京都に住む」に対し、 候補 {東京/都/に/住む} と {東京都/に/住む} を比べて、 確率(または最小コスト)の大きい方を出力する。 MeCab はこの最大化を Viterbi アルゴリズム で $O(n^2)$ または $O(nL)$(L: 辞書最長語長)で解く。
$\prod_{i=1}^{m} P(w_i \mid \text{context})$ — 右辺(確率の連鎖)
「各単語 $w_i$ が、 文脈(直前の単語 $w_{i-1}$ や品詞)の下で生起する条件付き確率の積」。 単語ごとの確率を掛け算するのは、 隠れマルコフモデル(HMM)の前提である条件付き独立を仮定するため。 SSDSE-B 都道府県名「北海道」が複合名詞辞書にあれば $P(\text{北海道} \mid \cdot) > P(\text{北}) \cdot P(\text{海道})$ となるため、 分割は「北海道」を 1 単語として選ぶ。 「対数」を取れば積→和になり数値安定性が向上、 これが「コスト最小化」の語源。
$\Sigma_w$ — 単語辞書(離散有限集合)
分割の候補となる単語の有限集合。 辞書の規模・粒度(短単位/長単位)で分割結果が変わる。 SSDSE-B 都道府県名「東京都」は IPADic でも UniDic 短単位でも「東京/都」と分かれ(下の 🐍 の実測)、 SudachiDict の C モード(長単位)だけが 1 語にする。 未知語(辞書にない語、 例:新製品名)は文字 N-gram モデルで生成確率を推定して救う。
$\text{cost}(w_i, w_{i-1})$ — 連接コスト(添字とエッジ)
隣り合う単語 $w_{i-1} \to w_i$ のつながりやすさを表す数値コスト。 名詞→助詞は低コスト(=確率大)、 動詞→名詞は文脈依存。 MeCab では学習データから条件付き対数確率の符号反転として計算され、 Viterbi 探索で全パスのコスト和を最小化する。 SSDSE-B「人口は約1400万人です」では、 「人口/は」「約/1400」「万/人」のような連接が低コストで通り、 1 つの整合的な分割が選ばれる。
📌 ポイント:単語分割の数式は動的計画法の典型例であり、 各記号が「辞書」「確率」「コスト」「探索」に 1 対 1 対応している。 数式を見たら「これは Viterbi の何ステップ目に対応するか」を即座に言える状態を目指すと、 NLP 全般の理解が深まる。

🔬 コストの式に値を入れる:「東京都に」は何語に分かれるか

上の「コスト」「ラティス」の説明を数値で確かめる。 単語コスト(小さいほど出やすい)を 東 6・京 6・東京 5・京都 4・都 3・東京都 6・に 1 とし、 名詞の後に名詞が続くときの連接コストを +2、 それ以外を 0 とする(説明用に置いた値)。 総コスト = 単語コストの合計 + 連接コストの合計 が最小の経路を選ぶ。

経路単語コスト連接コスト総コスト
東京都 / に6 + 107
東京 / 都 / に5 + 3 + 12(東京→都)11
東 / 京都 / に6 + 4 + 12(東→京都)13
東 / 京 / 都 / に6 + 6 + 3 + 12 + 220

最小は「東京都 / に」の 7。 ところが辞書で「東京都」のコストが 12 と大きければ(あるいは辞書に無ければ)、 「東京都 / に」は 13 になり、 「東京 / 都 / に」の 11 が選ばれる。 IPAdic の MeCab や Janome が「東京 / 都」と切る(🐍 の実測)のは、 この比べ合いの結果である。

🎯 このコードでやること:単語コストと連接コストの合計が最小になる分割を動的計画法で求める。 「東京都」の単語コストを 6 と 12 の 2 通りにして結果を比べる。

📥 入力例 文字列 東京都に、 単語コスト 東 6・京 6・東京 5・京都 4・都 3・東京都 6(または 12)・に 1、 名詞→名詞の連接コスト 2(説明用に置いた値)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 説明用に置いた単語コスト(小さいほど出やすい)。 品詞はすべて名詞、 「に」だけ助詞
WORD = {'東': 6, '京': 6, '東京': 5, '京都': 4, '都': 3, '東京都': 6, 'に': 1}
def link(prev, cur):                     # 連接コスト:名詞の後に名詞が続くと +2
    return 2 if prev != 'に' and cur != 'に' else 0

def best_path(s, word):
    # best[j] = (s[:j] までの最小コスト, 分割, 最後の語)
    best = {0: (0, [], None)}
    for j in range(1, len(s) + 1):
        cands = []
        for i in range(j):
            w = s[i:j]
            if w in word and i in best:
                c, seg, last = best[i]
                cands.append((c + word[w] + (link(last, w) if last else 0), seg + [w], w))
        if cands:
            best[j] = min(cands)
    return best[len(s)]

s = '東京都に'
for w_cost in [6, 12]:
    word = dict(WORD, 東京都=w_cost)
    cost, seg, _ = best_path(s, word)
    print(f'「東京都」のコスト {w_cost:>2}: 最小コスト {cost} → {" / ".join(seg)}')
📤 実行例(実測) 「東京都」のコスト 6: 最小コスト 7 → 東京都 / に 「東京都」のコスト 12: 最小コスト 11 → 東京 / 都 / に

💬 手計算の表と同じく、 コスト 6 なら最小 7 で「東京都 / に」、 12 なら最小 11 で「東京 / 都 / に」になる。 分割の結果は辞書のコストの数字しだいで入れ替わるので、 辞書と版を記録しない分割結果は再現できない。 実際の解析器は、 このコストを人手のラベル付きコーパスから学習している。

🧮 実データで計算してみる

「機械学習を学ぶ」を主要ツールで分割した結果をまず比較し、 その後 BPE マージ規則 を具体的な文字列ベクトルで手計算する。

ツール分割結果トークン数
MeCab+IPADIC機械 / 学習 / を / 学ぶ4
MeCab+NEologd機械学習 / を / 学ぶ3
SudachiPy(C)機械学習 / を / 学ぶ3
SentencePiece(BPE)▁機械 / 学習 / を / 学 / ぶ5

同じ文でもツールにより結果が違う。 用途に応じた選択が必要。 次に BPE マージを数値で確認する。

BPE マージ規則の手計算

数式:$\text{merge}(a,b) \Leftrightarrow (a,b)=\arg\max_{(x,y)}\text{count}(xy)$

ミニコーパス(都道府県名を題材にした 5 語のテキスト)で BPE を 1 ステップ追う。

📥 入力コーパス(文字単位に展開済み): ["北 海 道", "東 京 都", "東 京 湾", "東 海 道", "北 海 道"] ↑ 同じ文字列 "北 海 道" が 2 回 → 合計 5 文

Step 1 — 隣接ペア頻度を数える

全ペア一覧(コーパス全体を走査): (北,海) → 出現: 文①+文⑤ = 2 回 (海,道) → 出現: 文①+文④+文⑤ = 3 回 ← 最多 (東,京) → 出現: 文②+文③ = 2 回 (京,都) → 出現: 文② = 1 回 (京,湾) → 出現: 文③ = 1 回 (東,海) → 出現: 文④ = 1 回 → count("海道") = 3 が最大

Step 2 — 最頻ペアをマージ

マージ規則 R1: ("海","道") → "海道" マージ後のコーパス: ["北 海道", "東 京 都", "東 京 湾", "東 海道", "北 海道"] トークン総数: 15 → 12(3 減少)

Step 3 — 次ラウンド(確認)

次の最頻ペア: (北,海道) → 2 回 ← 今度はここ (東,京) → 2 回 ← 同率 (東,海道) → 1 回 マージ規則 R2: ("北","海道") → "北海道"(同率の場合は辞書順で先に出た方) 最終(2 マージ後): ["北海道", "東 京 都", "東 京 湾", "東 海道", "北海道"]

Python で再現(上記 Step 1〜3 と一致確認):

🎯 このコードでやること:5 文字列コーパス(都道府県名)で BPE の 1 ラウンド (ペア頻度の集計 → 最頻ペアのマージ) を手動実装し、 Step 1・2 の手計算値と一致することを確認する (Step 3 は corpus2 にもう一度 get_pairs と merge を当てれば再現できる)。

📥 入力データ(コーパス、文字単位に分割済み): corpus = [ ["北","海","道"], ["東","京","都"], ["東","京","湾"], ["東","海","道"], ["北","海","道"], ] # 5 文字列、 各文は 3 文字のリスト
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import Counter

# ミニコーパス(文字単位に分割済み)
corpus = [
    ["北","海","道"], ["東","京","都"], ["東","京","湾"],
    ["東","海","道"], ["北","海","道"],
]

def get_pairs(corp):
    cnt = Counter()
    for sent in corp:
        for i in range(len(sent)-1):
            cnt[(sent[i],sent[i+1])] += 1
    return cnt

def merge(corp, pair):
    merged = "".join(pair)
    return [[merged if s[i]==pair[0] and s[i+1]==pair[1] else s[i]
             for i in range(len(s))
             if not (i>0 and s[i-1]==pair[0] and s[i]==pair[1])] for s in corp]

pairs1 = get_pairs(corpus)
print("Step 1 ペア頻度:", pairs1.most_common(3))
best1 = pairs1.most_common(1)[0][0]
corpus2 = merge(corpus, best1)
print("Step 2 マージ後:", corpus2)
📤 実行結果: Step 1 ペア頻度: [(('海', '道'), 3), (('北', '海'), 2), (('東', '京'), 2)] Step 2 マージ後: [['北', '海道'], ['東', '京', '都'], ['東', '京', '湾'], ['東', '海道'], ['北', '海道']]

💬 Step 1 の手計算と Python 出力が完全一致。 count("海道")=3 が最大なので ("海","道") がマージされる。 これが BPE マージ規則 $\text{merge}(a,b)=\arg\max\text{count}(ab)$ の具体例。

🧮 別の文でトークン数を確かめる

同じ 1 文『東京都に住む人口』を、 分割手法ごとにトークン数がどう変わるかを実際の数値で確認します。 手法の違いが「動く感覚」で掴めることが目的です。

対象 計算結果
『東京都に住む人口』MeCab 分割数5(東京/都/に/住む/人口)
UniDic 短単位での分割5(東京/都/に/住む/人口。 unidic-lite で実測すると IPADIC と同じ分割になる)
BPE(GPT トークナイザ)でのトークン数トークナイザの語彙で変わる(このページでは実測していない)

🧮 数式に値を入れて手で計算する: 単語分割の正答率

合成データで単語境界正答率を計算する。

Step 1: 正解と予測

入力: "東京都港区六本木" 正解分割: ["東京都", "港区", "六本木"] (3 単語) 予測分割: ["東京", "都港区", "六本木"] (3 単語) 境界 (先頭から何文字目の後で切るか): 正解 {3, 5, 8}、 予測 {2, 5, 8} 境界一致: 5 と 8 → 2/3 (「東京都|港区」の 3 文字目の境界だけを外している)

Step 2: 単語単位の一致率 (再現率)

正解単語のうち予測に一致: {六本木} → 1 一致率 = 1/3 ≈ 0.333 (正解・予測とも 3 語なので、 再現率と適合率はどちらも 1/3)

🐍 Python で再現

🎯 このコードでやること:Step 1・2 の手計算(単語一致率 = 1/3 ≈ 0.333)を Python で再現し、 手計算と一致することを確認する。

📥 入力(手計算と同じ値): gold = ["東京都", "港区", "六本木"] # 正解分割 pred = ["東京", "都港区", "六本木"] # 予測分割
1
2
3
4
5
gold = ["東京都","港区","六本木"]
pred = ["東京","都港区","六本木"]
correct = len(set(gold) & set(pred))
acc = correct / len(gold)
print(f"単語一致率: {acc:.3f}")

📤 実行結果

単語一致率: 0.333

💬 手計算 (Step 2) 0.333 と Python 出力が完全一致。 境界で見ると 3 つ中 2 つが合っている (2/3) のに、 単語で見ると「六本木」しか合わない (1/3)。 境界を 1 か所ずらすだけで前後 2 語がまとめて不正解になるので、 単語単位の一致率は境界一致率より厳しい指標になる。

🧮 最長一致と「語数最小」を SSDSE-B-2026 の指標名で比べる

「一般病院数」(SSDSE-B-2026 の I510120)を、 説明用に置いた 6 語の辞書 {一般, 一般病, 病院, 病院数, 院, 数} で分ける。 5 文字の文字列には文字と文字の間が 4 か所あり、 それぞれ「切る/切らない」なので、 分け方の候補は $2^{4} = 16$ 通りある。

方法手計算の手順結果語数
最長一致位置 0 から辞書にある最も長い語「一般病」(3 文字)を取る → 位置 3 から「院」→ 位置 4 から「数」一般病 / 院 / 数3 語
語数最小(動的計画法)位置 j までの最小語数を左から求める。 j=2「一般」1 語、 j=5 は「一般」+「病院数」の 2 語が最小一般 / 病院数2 語

🎯 このコードでやること:最長一致法と、 語数が最小になる分割を動的計画法で求める方法を実装し、 同じ辞書・同じ文字列で結果を比べる。

📥 入力例 文字列 一般病院数、 辞書 {一般, 一般病, 病院, 病院数, 院, 数}(説明用に置いた辞書)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 教材用の小さな辞書(説明のために置いた 6 語)
DICT = {'一般', '一般病', '病院', '病院数', '院', '数'}
MAXLEN = max(len(w) for w in DICT)

def longest_match(s):
    out, i = [], 0
    while i < len(s):
        for L in range(min(MAXLEN, len(s) - i), 0, -1):
            if s[i:i + L] in DICT or L == 1:          # 辞書に無ければ 1 文字で進む
                out.append(s[i:i + L]); i += L; break
    return out

def min_words(s):
    # best[j] = s[:j] を分けるときの最小の語数と、その分け方
    best = [(0, [])] + [(10**9, None)] * len(s)
    for j in range(1, len(s) + 1):
        for i in range(max(0, j - MAXLEN), j):
            w = s[i:j]
            if (w in DICT or j - i == 1) and best[i][0] + 1 < best[j][0]:
                best[j] = (best[i][0] + 1, best[i][1] + [w])
    return best[len(s)][1]

s = '一般病院数'
print('最長一致 :', ' / '.join(longest_match(s)))
print('語数最小 :', ' / '.join(min_words(s)))
print('分け方の候補の数:', 2 ** (len(s) - 1))
📤 実行例(実測) 最長一致 : 一般病 / 院 / 数 語数最小 : 一般 / 病院数 分け方の候補の数: 16

💬 手計算どおり、 最長一致は最初に「一般病」を取ったため残りが「院 / 数」とばらけ、 語数最小は「一般 / 病院数」を選ぶ。 最長一致は先を見ずに今の位置で一番長い語を取るので、 辞書に紛らわしい長い語があると失敗する。 実際の形態素解析器は語数ではなく「単語コスト+連接コスト」の合計を最小にするが、 左から順に最良を積み上げる動的計画法(Viterbi)の骨組みは同じである。

🧮 BPE を SSDSE-B-2026 の指標名 109 個で学習させる

上の手計算では 5 語のミニコーパスで BPE を 2 回マージした。 同じ規則 $\text{merge}(a,b) \Leftrightarrow (a,b)=\arg\max\text{count}(xy)$ を、 SSDSE-B-2026 の実際の指標名 109 個(年度・地域コード・都道府県を除く日本語の列名)に 30 回当てると、 「頻度だけで決める」ことが何を生むかが見える。

🎯 このコードでやること:指標名 109 個を 1 文字ずつに分けた状態から、 最も多い隣接ペアをつなぐ操作を 30 回繰り返し、 つながった語と回数、 トークン総数の減り方、 代表的な指標名の分け方を表示する。

📥 入力例 SSDSE-B-2026.csv の 2 行目(日本語の列名)の 4 列目以降 109 個。 例:総人口、 合計特殊出生率、 延べ宿泊者数、 消費支出(二人以上の世帯)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
from collections import Counter

# 2 行目の日本語の列名を読み、年度・地域コード・都道府県を除いた 109 個の指標名を使う
names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=1).iloc[0])[3:]
words = [list(n) for n in names]                     # 1 文字ずつに分けた状態から始める
print('指標名', len(words), '個、 文字トークン', sum(len(w) for w in words), '個')

def merge_once(words):
    pairs = Counter()
    for w in words:
        pairs.update(zip(w, w[1:]))
    (a, b), c = pairs.most_common(1)[0]              # 最も多い隣接ペア
    out = []
    for w in words:
        i, new = 0, []
        while i < len(w):
            if i + 1 < len(w) and w[i] == a and w[i + 1] == b:
                new.append(a + b); i += 2
            else:
                new.append(w[i]); i += 1
        out.append(new)
    return out, a + b, c

for k in range(1, 31):
    words, tok, c = merge_once(words)
    if k <= 10 or k % 10 == 0:
        print(f'マージ {k:>2}: 「{tok}」({c} 回) → トークン {sum(len(w) for w in words)} 個')

for target in ['合計特殊出生率', '延べ宿泊者数', '一般病院数', '消費支出(二人以上の世帯)']:
    print(target, '→', ' | '.join(words[names.index(target)]))
📤 実行例(実測) 指標名 109 個、 文字トークン 974 個 マージ 1: 「数(」(18 回) → トークン 956 個 マージ 2: 「学校」(17 回) → トークン 939 個 マージ 3: 「人口」(15 回) → トークン 924 個 マージ 4: 「以上」(14 回) → トークン 910 個 マージ 5: 「者数」(11 回) → トークン 899 個 マージ 6: 「(二」(11 回) → トークン 888 個 マージ 7: 「(二人」(11 回) → トークン 877 個 マージ 8: 「(二人以上」(11 回) → トークン 866 個 マージ 9: 「(二人以上の」(11 回) → トークン 855 個 マージ 10: 「(二人以上の世」(11 回) → トークン 844 個 マージ 20: 「費(二人以上の世帯)」(9 回) → トークン 747 個 マージ 30: 「者数(日本人移動」(6 回) → トークン 677 個 合計特殊出生率 → 合 | 計 | 特 | 殊 | 出 | 生 | 率 延べ宿泊者数 → 延 | べ | 宿 | 泊 | 者数 一般病院数 → 一般 | 病 | 院 | 数 消費支出(二人以上の世帯) → 消 | 費 | 支 | 出 | (二人以上の世帯)

💬 最初につながるのは「数(」(18 回)で、 「転入者数(日本人移動者)」のような列名の、 語の境界をまたぐ「数」と括弧である。 BPE は意味を知らず、 回数の多い並びをつなぐだけなので、 言語学的な単語とは違う単位ができる。 「(二人以上の世帯)」は家計の 11 列に共通するので 10 回ほどで 1 つの塊になる一方、 1 回しか出てこない「合計特殊出生率」は 30 回マージしても 1 文字ずつのまま残る。 LLM のトークナイザが専門用語を細かく刻むのは、 学習コーパスでの出現回数が少ないためである。

🧮 境界の適合率・再現率・F 値を Janome の分割で計算する

上の「単語分割の正答率」は単語単位だった。 評価でよく使うのは、 区切りの位置(先頭から何文字目の後ろで切るか)を正解と比べる境界単位の適合率・再現率・F 値である。 「一般診療所数」を例に手で数える。

Step分割区切り・値
Step 1正解(人手)一般 / 診療所 / 数区切り {2, 5}
Step 2Janome一般 / 診療 / 所 / 数区切り {2, 4, 5}
Step 3比較一致 {2, 5} が 2、 余分 {4} が 1、 見落とし 0適合率 2/3 = 0.667、 再現率 2/2 = 1.000
Step 4F 値2 × 0.667 × 1.000 / (0.667 + 1.000)0.800

🎯 このコードでやること:SSDSE-B-2026 の指標名 5 個について、 人手で決めた正解と Janome(IPAdic 相当の辞書)の分割の区切り位置を比べ、 5 個まとめた適合率・再現率・F 値を計算する。

📥 入力例 指標名 5 個と人手の正解:合計特殊 / 出生率、 一般 / 診療所 / 数、 延べ / 宿泊者 / 数、 着工 / 建築物 / 数、 婚姻 / 件数(正解は指標名として意味のまとまりで区切ったもの)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from janome.tokenizer import Tokenizer

def bounds(tokens):
    """単語の区切り位置(先頭から何文字目の後ろか)の集合。 末尾は数えない"""
    pos, out = 0, set()
    for t in tokens[:-1]:
        pos += len(t); out.add(pos)
    return out

# 人手で決めた正解(指標名として意味のまとまりで区切ったもの)
gold = {'合計特殊出生率': ['合計特殊', '出生率'],
        '一般診療所数': ['一般', '診療所', '数'],
        '延べ宿泊者数': ['延べ', '宿泊者', '数'],
        '着工建築物数': ['着工', '建築物', '数'],
        '婚姻件数': ['婚姻', '件数']}
tk = Tokenizer()
tp = fp = fn = 0
for name, g in gold.items():
    p = [t.surface for t in tk.tokenize(name)]
    G, Pd = bounds(g), bounds(p)
    tp += len(G & Pd); fp += len(Pd - G); fn += len(G - Pd)
    print(f'{name}: Janome {" | ".join(p)}  正解の区切り {sorted(G)}  Janome の区切り {sorted(Pd)}')
prec, rec = tp / (tp + fp), tp / (tp + fn)
print(f'一致 {tp}・余分 {fp}・見落とし {fn} → 適合率 {prec:.3f}  再現率 {rec:.3f}  F 値 {2*prec*rec/(prec+rec):.3f}')
📤 実行例(実測) 合計特殊出生率: Janome 合計 | 特殊 | 出生 | 率 正解の区切り [4] Janome の区切り [2, 4, 6] 一般診療所数: Janome 一般 | 診療 | 所 | 数 正解の区切り [2, 5] Janome の区切り [2, 4, 5] 延べ宿泊者数: Janome 延べ | 宿泊 | 者 | 数 正解の区切り [2, 5] Janome の区切り [2, 4, 5] 着工建築物数: Janome 着工 | 建築 | 物 | 数 正解の区切り [2, 5] Janome の区切り [2, 4, 5] 婚姻件数: Janome 婚姻 | 件数 正解の区切り [2] Janome の区切り [2] 一致 8・余分 5・見落とし 0 → 適合率 0.615 再現率 1.000 F 値 0.762

💬 一般診療所数は手計算と同じ区切り {2, 4, 5} で、 余分な区切りが 4 文字目の 1 つ。 5 個まとめると一致 8・余分 5・見落とし 0 で、 適合率 0.615・再現率 1.000・F 値 0.762 になる。 Janome は正解の区切りを 1 つも見落とさない代わりに「診療 / 所」「宿泊 / 者」「建築 / 物」と細かく切りすぎる。 これは誤りというより、 IPAdic の単位が人手で決めた単位より短いということで、 評価の数字は正解をどの単位で作るかで大きく変わる(⚠️ の「評価の難しさ」)。

📝 理解度チェック

  1. 「合計特殊出生率」(7 文字)の分け方の候補は全部で何通りか。
    答え文字の間が 6 か所あるので 2⁶ = 64 通り。 文字数が増えると候補は指数的に増えるので、 動的計画法で探す。
  2. 上の BPE で、 最初のマージ「数(」の 18 回は何を数えたものか。
    答え109 個の指標名の中で、 「数」の直後に「(」が来る並びの出現回数。 語の境界をまたいでいても、 回数が多ければつながる。
  3. 正解の区切りが {2, 5}、 予測が {2, 4, 5} のときの適合率・再現率・F 値は。
    答え適合率 2/3 = 0.667、 再現率 2/2 = 1、 F 値 0.8。
  4. 辞書 {一般, 一般病, 病院, 病院数, 院, 数} で「一般病院」(4 文字)を最長一致で分けるとどうなるか。 語数最小ではどうか。
    答え最長一致は「一般病 / 院」(2 語)、 語数最小は「一般 / 病院」も「一般病 / 院」も 2 語で同数。 語数だけでは決まらず、 単語や連接のコストが必要になる。

🐍 Python 実装

日本語文を分かち書きする最小コード:

🎯 解説: 日本語テキストを単語の連なりに分解する「分かち書き」を 2 通り(MeCab / Janome)で実行。 MeCab は C 実装で速度重視(毎秒数十万トークン)、 Janome は Pure Python で導入容易。 -Owakati オプションは品詞情報を省き、 半角スペース区切りの文字列のみを返す軽量モード。 形態素解析の最も基本的な入口。
📥 入力例: text = '機械学習を学ぶ'(7 文字の日本語文) → 応用: 調査報告書テキストや地名・組織名などの前処理に使う → 実例: "北海道札幌市の人口は約 195 万人" → ['北海道','札幌','市','の','人口','は','約','195','万','人'] → 辞書は IPADIC(標準)/ NEologd(新語強化)/ UniDic(学術)から選択
1
2
3
4
5
6
7
8
9
10
11
12
13
# MeCab を使った分かち書き
import MeCab
import ipadic   # IPADIC 辞書(pip install ipadic)。辞書を明示しないと環境にある辞書で結果が変わる

tagger = MeCab.Tagger(ipadic.MECAB_ARGS + ' -Owakati')
text = '機械学習を学ぶ'
print(tagger.parse(text).strip())
# 出力: 機械 学習 を 学ぶ

# Janome(純Pythonで動く)
from janome.tokenizer import Tokenizer
t = Tokenizer()
print([tok.surface for tok in t.tokenize(text)])
📤 実行例(mecab-python3 + IPADIC、 Janome 0.5 で実測) 機械 学習 を 学ぶ ['機械', '学習', 'を', '学ぶ']
💬 読み方: 日本語は中国語と並び単語境界が明示されない言語のため、 検索・分類・要約のいずれにも分かち書きが必須。 ビタビアルゴリズムで「コスト最小経路」を選ぶ点が両ライブラリ共通。 BERT/SentencePiece 系のサブワード分割(WordPiece, BPE)は未知語に強く、 LLM 時代の主流だが、 品詞情報が失われるため文法解析には依然 MeCab が現役。 アプリ用途では辞書の世代固定(mecab-ipadic 2.7.0 等)と NFKC 正規化を必ずセットで運用する。

🐍 応用コード — 分かち書き結果で単語の頻度を数える

単語分割の最も典型的な応用は「分割 → 単語頻度の集計」です。 ここでは複数の日本語文を MeCab で形態素解析し、 名詞だけを取り出して出現頻度を数える最小コードを示します。 分割の粒度がそのまま頻度表を左右する様子を体感できます。

🎯 このコードでやること:3 文の日本語テキストを形態素解析し、 名詞の表層形を出現回数の多い順に表示する。

📥 入力テキスト(3 文): texts = [ '東京都に人が集まる', '大阪府にも人が多い', '東京都と大阪府は大都市だ', ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections import Counter
import MeCab
import ipadic   # IPADIC 辞書を明示(UniDic だと「大都市」が「大 / 都市」に割れる)

# 3 文の日本語テキスト
texts = [
    '東京都に人が集まる',
    '大阪府にも人が多い',
    '東京都と大阪府は大都市だ',
]

# 形態素解析して名詞だけ数える
tagger = MeCab.Tagger(ipadic.MECAB_ARGS)
freq = Counter()
for text in texts:
    node = tagger.parseToNode(text)
    while node:
        if node.feature.split(',')[0] == '名詞':
            freq[node.surface] += 1
        node = node.next

print(freq.most_common())
📤 実行例(mecab-python3 + IPADIC で実測): [('東京', 2), ('都', 2), ('人', 2), ('大阪', 2), ('府', 2), ('大都市', 1)]

💬 IPADIC では「東京都」「大阪府」が「東京 / 都」「大阪 / 府」に割れるので、 東京・都・人・大阪・府がそれぞれ 2 回、 「大都市」は 1 語のまま 1 回と数えられる (同じ回数の語は最初に出てきた順に並ぶ)。 分かち書きの後に品詞で名詞だけを絞り込むと、 文書中の主要語をそのまま頻度表にできる。 単語分割の品質がこの集計を直接左右する ── 「東京都」を 1 語にするか「東京 / 都」に割るかで、 頻度カウントの結果が変わる点に注意。

🐍 MeCab・Janome・Sudachi で同じ文を分割する

日本語の単語分割を MeCab / Janome / sudachi で比較します。 同じ文に対する分割の差を体感しましょう。

🎯 このコードでやること:「東京都に住む人口は約1400万人です」を MeCab / Janome / sudachi の 3 ツールで分割し、 辞書の違いによるトークン差を確認する。

📥 入力テキスト: text = '東京都に住む人口は約1400万人です' # 使用ライブラリ: MeCab (-Owakati), janome, sudachipy (A/C モード) # 想定環境: pip install mecab-python3 janome sudachipy sudachidict-core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import MeCab
import ipadic
from janome.tokenizer import Tokenizer as JanomeTok
from sudachipy import dictionary, tokenizer as st

text = '東京都に住む人口は約1400万人です'

# MeCab(IPADic)
m = MeCab.Tagger(ipadic.MECAB_ARGS + ' -Owakati')
print('MeCab :', m.parse(text).strip())

# Janome
jt = JanomeTok()
print('Janome:', ' '.join([t.surface for t in jt.tokenize(text)]))

# sudachi(A: 短単位, C: 長単位)
sd = dictionary.Dictionary().create()
for mode_name, mode in [('A', st.Tokenizer.SplitMode.A),
                         ('C', st.Tokenizer.SplitMode.C)]:
    toks = [m.surface() for m in sd.tokenize(text, mode)]
    print(f'sudachi-{mode_name}:', ' '.join(toks))
📤 実行例: MeCab : 東京 都 に 住む 人口 は 約 1400 万 人 です Janome: 東京 都 に 住む 人口 は 約 1400 万 人 です sudachi-A: 東京 都 に 住む 人口 は 約 1400万 人 です sudachi-C: 東京都 に 住む 人口 は 約 1400万 人 です ← 長単位 → mecab-python3 + IPADIC、 Janome 0.5、 SudachiPy 0.7 + sudachidict_core 20260723 で実測。 → 「東京都」は sudachi の C モードだけが 1 語にし、 「1400万」は sudachi が A・C とも数詞としてまとめる

💬 MeCab と Janome はどちらも IPADIC 系の辞書なので 11 トークンで完全に同じ分割になり、 sudachi は A モードで 10、 C モードで「東京都」をまとめて 9 トークンになる。 用途に応じて辞書と分割粒度を選択:検索なら短単位(SudachiDict-A / MeCab)、 固有表現抽出・統計指標名には長単位(SudachiDict-C)が有利。 「東京都」が 1 トークンになるかどうかが後段の集計結果を直接変える。

🐍 Janome で SSDSE-B-2026 の指標名 109 個を分割して数える

🎯 このコードでやること:指標名 109 個を Janome で分割し(記号は除く)、 1 つの指標名が何語になるか、 どの語が多くの指標名に出てくるか、 1 文字の語がいくつあるかを数える。

📥 入力例 SSDSE-B-2026.csv の 2 行目(日本語の列名)の 4 列目以降 109 個
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pandas as pd
from collections import Counter
from janome.tokenizer import Tokenizer

names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=1).iloc[0])[3:]
tk = Tokenizer()
toks = {n: [t.surface for t in tk.tokenize(n) if not t.part_of_speech.startswith('記号')] for n in names}

n_tok = Counter(len(v) for v in toks.values())
print('指標名', len(names), '個 → トークン', sum(map(len, toks.values())), '個(記号を除く)')
print('1 つの指標名のトークン数:', dict(sorted(n_tok.items())))
freq = Counter(t for v in toks.values() for t in dict.fromkeys(v))   # 1 つの指標名で 1 回だけ数える
print('多くの指標名に出てくる語:', freq.most_common(8))
single = [t for t in freq if len(t) == 1]
print('1 文字の語の異なり数:', len(single), ' 例:', sorted(single, key=lambda t: -freq[t])[:8])
# 「数」で終わる指標名のうち、 Janome が「数」を独立の語に切ったもの
ends = [n for n in names if n.endswith('数')]
print('「数」で終わる指標名', len(ends), '個のうち、 最後の語が「数」だけのもの', sum(toks[n][-1] == '数' for n in ends), '個')
📤 実行例(実測) 指標名 109 個 → トークン 526 個(記号を除く) 1 つの指標名のトークン数: {2: 11, 3: 26, 4: 20, 5: 16, 6: 11, 7: 10, 8: 9, 9: 4, 11: 2} 多くの指標名に出てくる語: [('数', 61), ('の', 19), ('者', 18), ('人口', 15), ('以上', 14), ('人', 13), ('二', 11), ('世帯', 11)] 1 文字の語の異なり数: 29 例: ['数', 'の', '者', '人', '二', '男', '女', '歳'] 「数」で終わる指標名 50 個のうち、 最後の語が「数」だけのもの 46 個

💬 109 個が 526 語に分かれ、 1 つの指標名は 2〜11 語(3 語が 26 個で最多)。 最も多くの指標名に出てくる語は「数」の 61 個で、 「数」で終わる 50 個のうち 46 個で「数」が独立の語になる。 1 文字の語は 29 種類あり、 「数」「者」「人」のような接尾的な語が単独のトークンとして大量に出る。 このまま語の出現回数を数えると、 意味の薄い「数」「の」「者」が上位を占めるので、 集計の前に分野別のストップワードで落とすか、 指標名をまとめて 1 語として扱う設計にする。 🖼 の図 2(トークンの文字数)と図 3(分野別のトークン数)は、 同じ分割結果を図にしたものである。

⚠️ よくある落とし穴

⚠️ 辞書のバージョン違いで再現性が崩れる
MeCab+IPADIC と MeCab+NEologd で結果が異なる。 → 使った辞書を必ず明記。
⚠️ 未知語の扱い
新語や固有名詞が「未知語」になり 1 文字ずつ分割される。 → 辞書追加 or サブワード化。
⚠️ 正規化を忘れる
半角/全角、 大文字/小文字、 異体字でトークンが分散。 → NFKC 正規化を前処理。
⚠️ 品詞情報を捨てる
分かち書きだけだと品詞による絞り込みができない。 → 形態素解析の結果まで保持。
⚠️ 英数字を分割しすぎ
URL や型番が細切れになる。 → 専用ルールで保護。

⚠️ 単語分割で絶対避けたい 10 のアンチパターン

  1. 辞書バージョンを記録しない: 半年後に再現できず、 分析の信頼性が崩れる。
  2. 正規化を後回しにする: 全角空白や BOM が混入し、 同じ語が複数トークンに分裂する。
  3. 未知語率を測らない: 知らぬ間に辞書外語が増え、 後段の集計が壊れる。
  4. ストップワードを汎用リストに任せる: 分野特有の単位語が頻度上位に居座る。
  5. 同義語統合をしない: 「サーバ」「サーバー」「server」がバラバラに集計される。
  6. 品詞情報を捨てる: 名詞だけ集計したい場面で動詞混入の混乱を招く。
  7. 1 つの辞書だけで運用する: 他辞書との比較がないと品質劣化に気付けない。
  8. 評価セットを更新しない: 新語や新ドメインの出現にカバレッジが追いつかない。
  9. 分割後のテキストを生のまま保存しない: 後で辞書を変更した時の比較が不可能になる。
  10. ダウンストリーム指標を見ない: 単語分割の F 値だけ高くても、 検索や分類の質が下がっていれば本末転倒。

📖 単語分割が支える日常 — SSDSE で読み解く 3 つのシーン

シーン 1:地域経済の自動レポート生成 ── 都道府県の経済政策レポートを自動生成するシステムを考える。 SSDSE-B-2026 の A4103_合計特殊出生率、 C3301_着工建築物数、 I5102_一般診療所数 を引いて文を作る場面で、 列名の単語分割が雑だと「合計・特殊・出生・率」と読まれて「合計特殊出生率」を別物として扱う失敗が起きる。 専門語を保持する SudachiDict-C を使い、 ユーザ辞書に「合計特殊出生率」「着工建築物数」を登録するだけで、 出力文の自然さが格段に上がる。

シーン 2:観光統計の検索インデックス ── 地方自治体の観光振興課が、 観光関連の統計を一括検索したい場面。 SSDSE-B-2026 の G7101_延べ宿泊者数 系列を含めた多数の列を、 単語分割を介してインデックス化する。 「宿泊者」と「延べ宿泊者数」が独立トークンとして登録されないと、 「宿泊者」で検索したときに「延べ宿泊者数」がヒットしなくなる。 これは検索系では致命的で、 SudachiDict の A モード(細粒度)を併用し、 両方のトークンを保持させるのが定石。

シーン 3:自由記述アンケートの感情分析 ── 「住み続けたい理由」を自由記述で収集するアンケートを、 ポジネガ分類する場面。 単語分割の精度が低いと、 否定語「ない」が動詞・形容詞の活用末尾と切れず、 「便利でない」が「便利」と誤判定される。 IPAdic と品詞情報の組み合わせで「否定」を明示的に抽出し、 BERT 系のセンチメント分類モデルへの入力品質を上げるのが王道。

これら 3 シーンに共通する教訓は、 「単語分割は分析の品質を左から支える基礎工事であり、 工事の手抜きは見えない場所で後段の精度を蝕む」という点だ。 早い段階で良い辞書とパイプラインに投資することが、 結局はもっとも安く高品質な分析につながる。

🩺 単語分割トラブル診療所 — 症状別ガイド 12 ケース

  1. 症状: 同じ列名が複数のトークンに分裂する。
    診断: 全角/半角の混在、 改行コードの差異。
    処方: NFKC 正規化+改行統一。
  2. 症状: 「総人口」「総 人口」が別語として集計される。
    診断: 列名内の不可視空白。
    処方: re.sub(r'\s+', '', s) で空白除去後に分割。
  3. 症状: 検索クエリ「宿泊者」で「延べ宿泊者数」がヒットしない。
    診断: トークンが完全一致でしか引けない。
    処方: SudachiDict-A の細粒度モードを併用。
  4. 症状: 同義語が別物として扱われる。
    診断: 同義語辞書未整備。
    処方: synonym.json を整備し、 後処理で正規化。
  5. 症状: 「サーバー」と「サーバ」が両方トークン化される。
    診断: 長音記号正規化未適用。
    処方: Sudachi の長音記号正規化機能をオン。
  6. 症状: 数値「1.5」が「1」「.」「5」に分かれる。
    診断: 数値解析ルール未調整。
    処方: 数値正規表現で事前マスク→分割→復元。
  7. 症状: 「消費支出(二人以上の世帯)」が「消費支出」「(二人以上の世帯)」に分かれない。
    診断: 括弧の取り扱い設定。
    処方: 括弧前後にスペースを挿入する前処理。
  8. 症状: 辞書ロードに数秒かかる。
    診断: 辞書サイズの肥大化。
    処方: 必要な辞書のみ事前選択、 サーバ起動時に 1 度だけロード。
  9. 症状: Docker コンテナ内で辞書が見つからない。
    診断: 辞書ファイルが COPY されていない。
    処方: Dockerfile の COPY 文に辞書ディレクトリを明示。
  10. 症状: Python のメモリが急増する。
    診断: 分析中に Tokenizer インスタンスを毎回作成。
    処方: Tokenizer はシングルトンで保持。
  11. 症状: テスト環境と本番環境で分割結果が異なる。
    診断: 辞書バージョンの差異。
    処方: pip lock / poetry lock で辞書バージョンを固定。
  12. 症状: 半年運用したらユーザ辞書が肥大化した。
    診断: 重複登録、 古い語彙の残存。
    処方: 月次で使用頻度の低い語を退役、 重複検査。

⚠️ NFKC 正規化で SSDSE-B-2026 の指標名の 4 割が変わる

🎯 このコードでやること:指標名 109 個に NFKC 正規化をかけて、 文字が変わるものと、 Janome の切れ目が変わるものを数える。 手入力の資料で起きやすい全角数字・半角カナの例も並べる。

📥 入力例 SSDSE-B-2026.csv の日本語の列名 109 個、 例として「15歳未満人口」「コンビニ数」(説明用に作った表記)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unicodedata
import pandas as pd
from janome.tokenizer import Tokenizer

nfkc = lambda s: unicodedata.normalize('NFKC', s)
names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=1).iloc[0])[3:]
changed = [n for n in names if nfkc(n) != n]
chars = sorted({c for n in names for c in n if nfkc(c) != c})
print('NFKC で文字が変わる指標名:', len(changed), '/', len(names), ' 変わる文字:', ' '.join(f'{c}→{nfkc(c)}' for c in chars))

# 正規化の前後で、 Janome の切れ目が変わる指標名
tk = Tokenizer()
cut = lambda s: [t.surface for t in tk.tokenize(s)]
moved = [n for n in changed if [nfkc(t) for t in cut(n)] != cut(nfkc(n))]
print('切れ目が変わる指標名:', len(moved), '個')
n = moved[0]
print('例', n)
print('  正規化前:', ' | '.join(cut(n)))
print('  正規化後:', ' | '.join(cut(nfkc(n))))
# 手入力の資料で起きがちな揺れ(全角数字・半角カナ)を NFKC がそろえる例
for s in ['15歳未満人口', 'コンビニ数']:
    print(s, '→', nfkc(s))
📤 実行例(実測) NFKC で文字が変わる指標名: 46 / 109 変わる文字: (→( )→) ~→~ 切れ目が変わる指標名: 6 個 例 転入者数(日本人移動者)(男) 正規化前: 転入 | 者 | 数 | ( | 日本人 | 移動 | 者 | ) | ( | 男 | ) 正規化後: 転入 | 者 | 数 | ( | 日本人 | 移動 | 者 | )( | 男 | ) 15歳未満人口 → 15歳未満人口 コンビニ数 → コンビニ数

💬 109 個のうち 46 個が NFKC で変わり、 変わる文字は全角の括弧「(」「)」と波ダッシュ「~」の 3 種類だけ。 ほとんどは括弧が半角になるだけだが、 括弧が 2 組続く 6 個(「転入者数(日本人移動者)(男)」など)では、 半角になった「)(」が 1 つの記号としてまとまり、 切れ目の数が変わる。 正規化は分割の結果そのものを変えることがあるので、 正規化してから分割する順番を固定し、 比べる資料の両方に同じ処理をかける。 全角数字の「15」や半角カナの「コンビニ」も、 そろえないと別の語として数えられる。

🗺 概念マップ — 単語分割の位置と関係

「単語分割」を中心に置き、 上流・並列・下流の概念を SVG で可視化する。 NLP パイプラインの中での単語分割の役割が一目で分かる。

単語分割 Word Segmentation 自然言語処理 NLP 上位概念 辞書ベース手法 MeCab / Janome サブワード手法 BPE / SentencePiece 形態素解析 品詞・原形付与 単語埋め込み Word2Vec / BERT 検索・文書分類 ダウンストリーム

矢印は「前提 → 単語分割」と「単語分割 → 後続処理」の関係を示す。 辞書ベースとサブワードは並列的選択肢、 形態素解析・単語埋め込み・検索/分類は下流の応用。

ノード具体例関係
自然言語処理 (NLP)テキスト前処理・BERT・LLM上位概念 — 単語分割を包含
辞書ベース手法MeCab+IPADIC, Janome, SudachiPy並列選択 — 古典的実装
サブワード手法BPE, SentencePiece, WordPiece並列選択 — LLM 時代の主流
形態素解析品詞付与・原形復元・読み下流 — 単語分割を拡張
単語埋め込みWord2Vec, fastText, BERT下流 — 分割後に適用
検索・文書分類Elasticsearch, TF-IDF, scikit-learn下流 — 最終応用

🧩 単語分割アルゴリズム

手法特徴実装例
最長一致法辞書から最も長くマッチする語を取る初期 MeCab・古典 IME
最小コスト法単語コスト + 接続コストの最小化MeCab (Viterbi)
CRF条件付き確率場による系列ラベリングCRF++、 KyTea
BiLSTM-CRF深層学習による系列ラベリング2015–2018 の標準
BERT 系事前学習モデルでファインチューニング2020 以降の SOTA
サブワードBPE、 SentencePiece、 WordPiece、 Unigram LMLLM 用トークナイザ

🧰 日本語形態素解析ツール比較

ツール辞書特徴
MeCabIPADic / UniDic / NEologdC++ 高速。 デファクト標準
JanomeIPADic 内蔵純 Python・インストール容易
SudachiPySudachi 辞書(多粒度)3 種類の分割モード(A/B/C)
JUMAN++京大独自RNN 言語モデル併用
KuromojiIPADic 系Java 系・Lucene/Solr で標準
fugashiMeCab ラッパーPython から MeCab を扱う標準

🔗 隣接手法への橋渡し

単語分割 (word segmentation、 形態素解析) は日本語 NLP の最重要前処理として、 以下と接続する。

日本語は単語境界が明示されないため、 単語分割は NLP の必須前処理。 辞書 (IPADIC, UniDic, NEologd) の選択で結果が大きく変わる ― 例: 「東京都」を 1 単語にするか 「東京/都」 で 2 単語にするか。 SSDSE 関連の自治体ニュース解析では、 NEologd 辞書で新語・地名対応が標準。

数で見ると、 SSDSE-B-2026 の指標名 109 個を Janome で分けた 631 トークンは異なり 139 語で、 これがそのまま TF-IDF や単語埋め込みの語彙になる。 同じ 109 個を 1 文字ずつにすれば語彙は文字の種類だけに減るが、 「人口」「世帯」のような意味のまとまりは下流のモデルが自分で学び直すことになる。

🌳 手法選択フロー

単語分割ツールの選択は「言語」「精度 vs 速度」「環境」で決まる。

  1. 対象言語は? 日本語 → MeCab/Sudachi/Janome、 中国語 → jieba/HanLP、 BERT 系の事前学習用 → SentencePiece (言語横断)
  2. 精度重視か速度重視か? 精度 → Sudachi (Mode A/B/C 切替可)、 速度 → MeCab (C++、 高速)、 ピュア Python → Janome (環境依存少)
  3. 辞書の選択は? 標準語 → IPADIC、 現代語・新語 → mecab-ipadic-NEologd、 学術 → UniDic、 業界特化 → 独自辞書追加
  4. BERT 等の事前学習モデル使用 → SentencePiece / WordPiece (サブワード分割) を選び、 元のトークナイザを尊重
  5. OOV (未知語) 対応 → サブワード分割、 文字単位 fallback、 ユーザー辞書追加

SSDSE 関連の都道府県ニュース解析では「MeCab + NEologd」が定石。 BERT を使う場合は事前学習時のトークナイザを必ず合わせる。

SSDSE-B-2026 の指標名で当てはめると次のようになる。 指標名を 1 つの概念として検索・突き合わせしたいなら、 分割せずに列名のまま扱うか、 長単位(Sudachi の C モードやユーザー辞書)を選ぶ。 名詞の出現回数を数えるなら形態素解析を使い、 Janome で「数」が 61 個の指標名に出るような接尾的な語をストップワードで落とす。 LLM に列名を渡すときはそのモデルのサブワード分割に従うしかないので、 出現回数の少ない専門用語ほど細かく刻まれること(🧮 の BPE で「合計特殊出生率」が 7 文字のまま残った例)を前提に、 列名の説明文を添える。

🎨 直感をもう一段深める — なぜ「区切る」だけで難しいのか

単語分割の直感は「文を単語に切る」ですが、 切る=境界を 1 本引くたびに意味が確定してしまう点が本質です。 英語なら I have a pen のように空白がすでに境界を教えてくれますが、 日本語・中国語・タイ語などは境界が明示されない(unsegmented)言語で、 書き手は境界を書かないまま「読み手が復元できる」ことを前提にしています。 単語分割とは、 この省略された境界情報を機械が復元するタスクだと捉えると腑に落ちます。

🎨 「境界を引く問題」としての単語分割

長さ $n$ の文字列には、 文字と文字の間が $n-1$ 箇所あります。 各すき間で「区切る/区切らない」の 2 択なので、 分割の候補は理論上 $2^{\,n-1}$ 通り。 だから単語分割は「膨大な候補から尤もらしい 1 つを選ぶ探索問題」であり、 だからこそ辞書・確率・コスト・動的計画法(Viterbi)が総動員されます。 「🔬 記号・式を言葉で読み解く」の $\arg\max$ は、 この $2^{n-1}$ 通りを賢く枝刈りして最良の 1 本を返す装置です。

🎨 形態素解析の中の「単語分割」

日本語の形態素解析は通常 3 つの仕事を同時にこなします:(1) 分割(境界を引く)、 (2) 品詞付与(各語に名詞・動詞などを割り当てる)、 (3) 原形化(「住ん」→「住む」)。 単語分割はこのうち (1) だけを取り出したもので、 MeCab の -Owakati はまさに「(1) だけ出力せよ」というモードです。 つまり単語分割は形態素解析の最小の部分集合であり、 NLP 前処理の一番最初の関門。 ここでのミスは TF-IDF・N-gram・単語埋め込み・分類器と、 下流のすべてに波及します。

🎨 3 系統の設計思想を一言で

系統発想の核未知語への態度
辞書ベース「知っている語」を並べて最良の敷き詰めを探す辞書に無い=原則見えない(別途救済が要る)
統計(HMM/CRF)文脈から境界の確率を学習で推定文字素性で「知らない語」も推定できる
ニューラル/サブワード語をさらに小さい断片に割り、 断片は必ず既知にする断片化で未知語を原理的に消す

辞書ベースは「知っている語で敷き詰めるパズル」、 統計は「境界に賭ける確率」、 サブワードは「そもそも未知語が生じないよう単位を小さくする」。 同じ問題への 3 つの思想だと押さえると、 ツール選択の軸がぶれません。

💡 直感の要:単語分割は「切る」より「候補の中から選ぶ」タスク。 選ぶ基準(辞書・確率・断片の既知性)が変われば、 同じ文でも答えが変わる —— これが後述の落とし穴すべての根っこです。

⚠️ 落とし穴を深掘り(重要)— 分割は「正解が一意でない」

単語分割で最も痛い誤解は「正しい分割が 1 つある」という思い込みです。 実際は目的・辞書・粒度の選び方で「正解」が動くため、 評価も再現も難しくなります。 既出の「よくある落とし穴」を踏まえ、 分割固有の罠を体系立てて深掘りします。

⚠️ 1. 未知語(OOV)— 新語・固有名詞・専門語

辞書に無い語は、 辞書ベースだと1 文字ずつバラバラに落ちがちです。 架空例(説明用):新商品名「ソラミドリ茶」が未知語だと ソ / ラ / ミ / ド / リ / 茶 と砕け、 頻度集計でも検索でも消えてしまう。 対策は (a) ユーザー辞書に追加、 (b) 統計モデル(CRF)の文字素性で救済、 (c) サブワード化。 SSDSE-B の列名は「総人口(A1101)」「日本人人口(A1102)」「出生数(A4101)」のように複合語の造語で、 辞書世代によっては「延べ宿泊者数」が 1 語にも「延べ/宿泊/者数」にも割れます。

⚠️ 2. 曖昧性 — 同じ文が複数に読める

境界の引き方で意味が変わる典型(架空の説明例):「うらにわにはにわにわとりがいる」、 「ここではきものをぬぐ」(「ここで/履物を/脱ぐ」か「ここでは/着物を/脱ぐ」か)。 人間は文脈で解けても、 辞書+コスト最小だけではもっともらしい方に倒れるだけで、 常に正解とは限りません。 曖昧性は「分割は言語理解と地続き」であることの証拠です。

⚠️ 3. 分割単位の選択 — 形態素 vs 単語 vs サブワード

「単語」の定義自体が揺れます。 UniDic の短単位は「東京/都」、 長単位や NEologd は「東京都」を 1 語。 Sudachi は A(細)/B(中)/C(粗)と粒度を切替可能。 検索では細かい方が再現率、 分類では粗い方が意味のまとまりに有利、 と下流タスクで最適粒度が違う。 「どの単位が正しいか」ではなく「この目的にどの単位が合うか」で選ぶのが正解です。

⚠️ 4. 辞書依存・ドメイン適応・表記ゆれ

罠症状(架空例で説明)対策
辞書世代差IPADIC と NEologd で結果が変わり再現不能辞書名・版を固定して明記(例 mecab-ipadic 2.7.0)
ドメイン不適合医療・法律・自治体語が一般辞書で砕けるドメイン辞書追加・追加学習
表記ゆれ「サーバ/サーバー」「1400/1400」が別トークンにNFKC 正規化を分割の前段に必ず入れる

⚠️ 5. 評価の難しさ — 何と比べて「正しい」のか

分割の評価は境界 F 値(正解境界と予測境界の一致)や単語 F 値で測りますが、 (a) そもそも「正解分割(ゴールド)」が辞書規約に依存し、 (b) 短単位で作った正解を長単位の出力で測ると不当に低く出ます。 「🧮 実データで計算してみる」の単語一致率 1/3 は、 まさに正解と予測の分割規約がずれると精度が崩れることの最小実例です。 評価するときは「どの規約のゴールドか」「境界単位か単語単位か」を先に固定するのが鉄則です。

📌 落とし穴の総括:未知語・曖昧性・粒度・辞書依存・評価、 5 つの罠はすべて「分割の正解は一意でない」という一点から派生します。 だから実務では「辞書と版」「粒度(短/長, Sudachi A/B/C)」「正規化」「評価規約」の 4 点を先に決めて記録することが、 再現性の生命線になります。

🚀 発展 — 辞書ベースからサブワードまでの技術地図

単語分割の手法は「ルール/統計/ニューラル」の順に発展し、 いまは形態素解析器(MeCab/Sudachi)とサブワード(BPE/SentencePiece)の併用が実務標準です。 それぞれの原理と勘所を地図として整理します。

🚀 1. 辞書ベース — 最長一致とコスト最小

最長一致法(greedy longest-match)は「その位置から始まる辞書語のうち一番長いものを貪欲に選ぶ」素朴な方法。 高速ですが局所最適で誤りやすい。 これを大域最適化したのがコスト最小法(ラティス+ Viterbi)で、 文の全分割候補をグラフ(ラティス)に展開し、 「単語コスト+連接コスト」の総和が最小の経路を動的計画法で選びます。 MeCab の中核はこれで、 「📐 定義 / 数式」の $\arg\max P(W\mid S)$ を対数コスト最小化として解いています。 下の「🎮 触って理解する」で最長一致とコスト最小の差を手で体験できます。

🚀 2. 統計的手法 — HMM と CRF

HMMは「単語列を隠れ状態、 文字列を観測」とみなし、 遷移確率と出力確率の積を最大化。 CRF(条件付き確率場)は各文字に「単語の先頭/中間/末尾(BIES など)」ラベルを付ける系列ラベリングとして分割を解き、 周囲の文字を素性として使えるため未知語に強い。 現代 MeCab の学習も CRF ベースで、 「辞書+統計」のハイブリッドになっています。

🚀 3. ニューラル系列ラベリング

BiLSTM-CRF や BERT ベースの文字レベル分類器は、 文脈埋め込みで境界を推定します。 精度は高い一方、 モデルが重く辞書の即時更新が効きにくい。 実務では「速度・更新性が要る前処理は MeCab/Sudachi、 精度が要る解析はニューラル」と使い分けるのが定番です。

🚀 4. サブワード — BPE / WordPiece / SentencePiece

LLM 時代の主役。 発想は「単語より小さい断片に割り、 断片語彙を有限に固定すれば未知語が原理的に消える」。 「🧮 実データで計算してみる」の BPE 手計算(海道 をマージ)がまさにこの原理です。 三者の違いを一言で:

手法マージ/分割の基準代表
BPE最頻の隣接ペアを貪欲にマージGPT 系
WordPiece尤度(言語モデル的スコア)が最大化するペアをマージBERT
Unigram LM大きな語彙から確率の低い断片を削っていくSentencePiece(既定)

SentencePiece は空白も 1 記号(▁)として扱い、 生テキストから直接学習できるため言語非依存。 だから日本語でも「事前分かち書き不要」で回せます。

🚀 5. 下流タスクへの影響

分割は前処理なので、 その良し悪しは静かに全下流へ伝播します。 TF-IDF / N-gram:トークンの粒度が語彙サイズと頻度分布を直接決める。 単語埋め込み:分割単位が語彙になるので、 粒度がベクトル空間の解像度を決める。 検索:登録トークンの粒度が再現率と適合率のトレードオフを左右する。 分類・要約:意味のまとまりが粗すぎても細かすぎても性能が落ちる。 「分割は独立した工程」ではなく下流と一体で最適化する対象だと捉えるのが上級者の視点です。

📌 発展の要:技術は「ルール→統計→ニューラル→サブワード」と進みましたが、 新しい=常に良い、 ではありません。 品詞・原形が要る解析は今も形態素解析器が現役、 LLM 前処理はサブワード。 目的から逆算して手法を選ぶのが最終的な発展形です。

🎮 触って理解する — 最長一致法と分割の曖昧性

日本語は空白が無いので、 まず「どこで区切るか」を決めねばなりません。 最も素朴な方法が 最長一致法(greedy longest-match)です。 左から順に、 その位置から始まる辞書語のうち一番長いものを貪欲に選び、 選んだぶんだけ右へ進む —— これを繰り返すだけ。 ここでは実際の解析器は使わず、 教材用に手で固定した小さな辞書(下に全語を明記)を用いた決定的(毎回同じ結果)なデモで、 (a) 1 文字ずつ最長一致を探す過程、 (b) 貪欲が間違える例とコスト最小(語数最小)による正しい分割との対比、 (c) 辞書に無い語(未知語)の扱い、 を体感します。 例文はすべて架空の固定例です。

📖 教材用固定辞書(この 13 語だけを使う)

※ 実際の辞書(IPADIC 約 35 万語, UniDic 約 75 万語)とは無関係の、 デモ専用の極小辞書です。 語彙をわざと絞ることで「辞書に無い語=未知語」の挙動も観察できます。

① 最長一致法を 1 ステップずつ追う

例文を選び、 スライダー(またはボタン)を動かすと、 左端から 位置ごとに「その場所から始まる辞書語」を長さ 1・2・3 と調べ、 一番長いものを選ぶ 様子が 1 手ずつ見えます。 緑=確定済み、 太線=今まさに選ばれた最長語、 赤=辞書に無く未知語として 1 文字だけ進む場合です。

これまでに確定した分割:

② 貪欲の失敗 vs コスト最小(語数最小)

最長一致は「今この場で一番長い語」を選ぶだけなので、 先を見ていない。 その結果、 最初に長い語を取ったせいで残りがバラバラになることがあります。 一方 語数が最小になる分割を全体最適で探すと(動的計画法。 実装は決定的)、 より自然な区切りが得られることがあります。 下は選択中の例文での両者の比較です。

最長一致法(貪欲)
語数最小(コスト最小・全体最適)

※ これは簡易デモです。 本デモの辞書・分割結果は教材用に手で固定した値であり、 特定ツールの出力を再現するものではありません。 実運用の解析器は数十万語規模の辞書と、 学習で推定した連接コスト表(HMM/CRF)を用います。

🧭 直感・落とし穴・発展

直感 — 最長一致は「辞書という物差しを頼りに、 一番長く当てはまる語で左から順に覆っていく」だけの単純ルール。 実装が容易で高速、 辞書がよく整備された定型テキストなら十分実用になります。 ①で「長さ 1・2・3 と試して最長を取る」走査を目で追えたはずです。

落とし穴 — ①貪欲の近視眼:研究生命体 で最初に「研究生」を取ると残りが「命/体」に割れてしまう(②参照)。 局所最適が全体最適とは限りません。 ②本質的曖昧性:くるまでまつ は「車で待つ」「来るまで待つ」の 2 通りに読め、 語数が同数だと語数最小でも決められず、 頻度・連接コストが必要。 ③未知語:辞書に無い語(タワー など新語・固有名詞)は 1 文字ずつバラバラに過分割され、 これは貪欲・語数最小のどちらでも起きる辞書ベース共通の限界。 ④辞書依存:語彙を変えれば区切りも変わる —— 上の極小辞書がそれを誇張して見せています。

発展 — 近視眼を克服するのがコスト最小化+Viterbi 探索で、 各語の生起コストと隣接語の連接コストの総和が最小の経路を全体最適で選びます(形態素解析のページで品詞付きラティスとして詳説)。 コストを人手でなく統計的に学習するのが HMM→CRF→ニューラル分割(BiLSTM/Transformer)の流れ。 隣接語の確率は N-gram・N-gram モデルで捉えます。 一方 BERT/GPT 系は辞書に頼らず サブワード分割(BPE / WordPiece / SentencePiece)で、 未知語を既知の部分文字列の組合せに分解して「未知語ゼロ」を実現します(本文「実データで計算」の BPE 手計算を参照)。

🔗 関連ページ

ここで見た「近視眼の克服=コスト最小の全体最適」を品詞付きで深掘りする 形態素解析(ラティス・Viterbi)、 連接コストの土台となる N-gram・N-gram モデル、 上位の枠組み 自然言語処理 と合わせて読むと、 「切る → つなぎを確率で見る → モデル化」の流れがつながります。