このページで数える量:分け方の候補(n 文字なら 2n−1 通り)、 単語コストと連接コストの合計、 BPE の隣接ペアの出現回数、 区切り位置の適合率・再現率・F 値。 題材は SSDSE-B-2026 の指標名 109 個(「合計特殊出生率」「延べ宿泊者数」など)で、 Janome(IPAdic 相当)で分けると 526 語になる。
別名・略称:分かち書き
🍰 まずはやさしく
文章を単語ごとに切り分ける作業です。
コンピュータが言葉を理解するために使います。
スマホの予測変換などの仕組みに似ています。
単語分割の基本と代表的な道具を学びます。
単語分割(Word Segmentation):文章を単語に分割する処理
🍰 まずはやさしく
文章をバラバラにする最初のステップです。
単語の数を数えるなどの準備のために行います。
「東京都に住んでいる」という文を分けます。
AIがどのように言葉を切るかを見ていきましょう。
🍰 まずはやさしく
言葉の切り分け方にはいくつかの種類があります。
状況に合わせて最適な方法を選ぶためです。
部活の報告書をどう分けるか考えるようなものです。
3つの切り分け方の特徴と違いを解説します。
| アプローチ | 代表ツール | 特徴 |
|---|---|---|
| 辞書+ラティス探索 | MeCab, Janome | 速い、 辞書品質に依存 |
| 統計モデル(CRF) | MeCab+IPADIC, SudachiPy | 未知語に強い |
| サブワード(BPE/SentencePiece) | SentencePiece, HuggingFace | 未知語ゼロ、 LLM標準 |
「東京都に住んでいる」東京 / 都 / に / 住ん / で / いる(6トークン)▁東京 / 都 / に / 住 / んで / いる(6トークン、 サブワード)I'm / living / in / Tokyo🍰 まずはやさしく
最も正しい分け方を確率で決める計算です。
数学的に正しい答えを導き出すために使います。
テストの正解を論理的に導く感覚に似ています。
分割を決めるための数式について詳しく読みます。
単語分割は確率最大の単語列を探す問題として定式化できます。
単語分割の核心は 「文字列 $s$ を、 単語列 $w_1 w_2 \cdots w_m$ に最も尤もらしく分けるパス」を求める動的計画問題です。 数式の各記号がどのアルゴリズム段階(辞書引き/コスト計算/Viterbi 探索)に対応するかを丁寧に追うと、 MeCab・Janome・sudachi の挙動の差まで根本から納得できます。 ここでは 4 つの主要記号を 1 つずつ日本語で読み解きます。
上の「コスト」「ラティス」の説明を数値で確かめる。 単語コスト(小さいほど出やすい)を 東 6・京 6・東京 5・京都 4・都 3・東京都 6・に 1 とし、 名詞の後に名詞が続くときの連接コストを +2、 それ以外を 0 とする(説明用に置いた値)。 総コスト = 単語コストの合計 + 連接コストの合計 が最小の経路を選ぶ。
| 経路 | 単語コスト | 連接コスト | 総コスト |
|---|---|---|---|
| 東京都 / に | 6 + 1 | 0 | 7 |
| 東京 / 都 / に | 5 + 3 + 1 | 2(東京→都) | 11 |
| 東 / 京都 / に | 6 + 4 + 1 | 2(東→京都) | 13 |
| 東 / 京 / 都 / に | 6 + 6 + 3 + 1 | 2 + 2 | 20 |
最小は「東京都 / に」の 7。 ところが辞書で「東京都」のコストが 12 と大きければ(あるいは辞書に無ければ)、 「東京都 / に」は 13 になり、 「東京 / 都 / に」の 11 が選ばれる。 IPAdic の MeCab や Janome が「東京 / 都」と切る(🐍 の実測)のは、 この比べ合いの結果である。
🎯 このコードでやること:単語コストと連接コストの合計が最小になる分割を動的計画法で求める。 「東京都」の単語コストを 6 と 12 の 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 で「東京 / 都 / に」になる。 分割の結果は辞書のコストの数字しだいで入れ替わるので、 辞書と版を記録しない分割結果は再現できない。 実際の解析器は、 このコストを人手のラベル付きコーパスから学習している。
「機械学習を学ぶ」を主要ツールで分割した結果をまず比較し、 その後 BPE マージ規則 を具体的な文字列ベクトルで手計算する。
| ツール | 分割結果 | トークン数 |
|---|---|---|
| MeCab+IPADIC | 機械 / 学習 / を / 学ぶ | 4 |
| MeCab+NEologd | 機械学習 / を / 学ぶ | 3 |
| SudachiPy(C) | 機械学習 / を / 学ぶ | 3 |
| SentencePiece(BPE) | ▁機械 / 学習 / を / 学 / ぶ | 5 |
同じ文でもツールにより結果が違う。 用途に応じた選択が必要。 次に BPE マージを数値で確認する。
数式:$\text{merge}(a,b) \Leftrightarrow (a,b)=\arg\max_{(x,y)}\text{count}(xy)$
ミニコーパス(都道府県名を題材にした 5 語のテキスト)で BPE を 1 ステップ追う。
Python で再現(上記 Step 1〜3 と一致確認):
🎯 このコードでやること:5 文字列コーパス(都道府県名)で BPE の 1 ラウンド (ペア頻度の集計 → 最頻ペアのマージ) を手動実装し、 Step 1・2 の手計算値と一致することを確認する (Step 3 は corpus2 にもう一度 get_pairs と merge を当てれば再現できる)。
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 の手計算と 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・2 の手計算(単語一致率 = 1/3 ≈ 0.333)を Python で再現し、 手計算と一致することを確認する。
1 2 3 4 5 | gold = ["東京都","港区","六本木"] pred = ["東京","都港区","六本木"] correct = len(set(gold) & set(pred)) acc = correct / len(gold) print(f"単語一致率: {acc:.3f}") |
💬 手計算 (Step 2) 0.333 と Python 出力が完全一致。 境界で見ると 3 つ中 2 つが合っている (2/3) のに、 単語で見ると「六本木」しか合わない (1/3)。 境界を 1 か所ずらすだけで前後 2 語がまとめて不正解になるので、 単語単位の一致率は境界一致率より厳しい指標になる。
「一般病院数」(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)) |
💬 手計算どおり、 最長一致は最初に「一般病」を取ったため残りが「院 / 数」とばらけ、 語数最小は「一般 / 病院数」を選ぶ。 最長一致は先を見ずに今の位置で一番長い語を取るので、 辞書に紛らわしい長い語があると失敗する。 実際の形態素解析器は語数ではなく「単語コスト+連接コスト」の合計を最小にするが、 左から順に最良を積み上げる動的計画法(Viterbi)の骨組みは同じである。
上の手計算では 5 語のミニコーパスで BPE を 2 回マージした。 同じ規則 $\text{merge}(a,b) \Leftrightarrow (a,b)=\arg\max\text{count}(xy)$ を、 SSDSE-B-2026 の実際の指標名 109 個(年度・地域コード・都道府県を除く日本語の列名)に 30 回当てると、 「頻度だけで決める」ことが何を生むかが見える。
🎯 このコードでやること:指標名 109 個を 1 文字ずつに分けた状態から、 最も多い隣接ペアをつなぐ操作を 30 回繰り返し、 つながった語と回数、 トークン総数の減り方、 代表的な指標名の分け方を表示する。
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)])) |
💬 最初につながるのは「数(」(18 回)で、 「転入者数(日本人移動者)」のような列名の、 語の境界をまたぐ「数」と括弧である。 BPE は意味を知らず、 回数の多い並びをつなぐだけなので、 言語学的な単語とは違う単位ができる。 「(二人以上の世帯)」は家計の 11 列に共通するので 10 回ほどで 1 つの塊になる一方、 1 回しか出てこない「合計特殊出生率」は 30 回マージしても 1 文字ずつのまま残る。 LLM のトークナイザが専門用語を細かく刻むのは、 学習コーパスでの出現回数が少ないためである。
上の「単語分割の正答率」は単語単位だった。 評価でよく使うのは、 区切りの位置(先頭から何文字目の後ろで切るか)を正解と比べる境界単位の適合率・再現率・F 値である。 「一般診療所数」を例に手で数える。
| Step | 分割 | 区切り・値 | |
|---|---|---|---|
| Step 1 | 正解(人手) | 一般 / 診療所 / 数 | 区切り {2, 5} |
| Step 2 | Janome | 一般 / 診療 / 所 / 数 | 区切り {2, 4, 5} |
| Step 3 | 比較 | 一致 {2, 5} が 2、 余分 {4} が 1、 見落とし 0 | 適合率 2/3 = 0.667、 再現率 2/2 = 1.000 |
| Step 4 | F 値 | 2 × 0.667 × 1.000 / (0.667 + 1.000) | 0.800 |
🎯 このコードでやること:SSDSE-B-2026 の指標名 5 個について、 人手で決めた正解と Janome(IPAdic 相当の辞書)の分割の区切り位置を比べ、 5 個まとめた適合率・再現率・F 値を計算する。
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}') |
💬 一般診療所数は手計算と同じ区切り {2, 4, 5} で、 余分な区切りが 4 文字目の 1 つ。 5 個まとめると一致 8・余分 5・見落とし 0 で、 適合率 0.615・再現率 1.000・F 値 0.762 になる。 Janome は正解の区切りを 1 つも見落とさない代わりに「診療 / 所」「宿泊 / 者」「建築 / 物」と細かく切りすぎる。 これは誤りというより、 IPAdic の単位が人手で決めた単位より短いということで、 評価の数字は正解をどの単位で作るかで大きく変わる(⚠️ の「評価の難しさ」)。
日本語文を分かち書きする最小コード:
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 で形態素解析し、 名詞だけを取り出して出現頻度を数える最小コードを示します。 分割の粒度がそのまま頻度表を左右する様子を体感できます。
🎯 このコードでやること:3 文の日本語テキストを形態素解析し、 名詞の表層形を出現回数の多い順に表示する。
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()) |
💬 IPADIC では「東京都」「大阪府」が「東京 / 都」「大阪 / 府」に割れるので、 東京・都・人・大阪・府がそれぞれ 2 回、 「大都市」は 1 語のまま 1 回と数えられる (同じ回数の語は最初に出てきた順に並ぶ)。 分かち書きの後に品詞で名詞だけを絞り込むと、 文書中の主要語をそのまま頻度表にできる。 単語分割の品質がこの集計を直接左右する ── 「東京都」を 1 語にするか「東京 / 都」に割るかで、 頻度カウントの結果が変わる点に注意。
日本語の単語分割を MeCab / Janome / sudachi で比較します。 同じ文に対する分割の差を体感しましょう。
🎯 このコードでやること:「東京都に住む人口は約1400万人です」を MeCab / Janome / sudachi の 3 ツールで分割し、 辞書の違いによるトークン差を確認する。
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 と Janome はどちらも IPADIC 系の辞書なので 11 トークンで完全に同じ分割になり、 sudachi は A モードで 10、 C モードで「東京都」をまとめて 9 トークンになる。 用途に応じて辞書と分割粒度を選択:検索なら短単位(SudachiDict-A / MeCab)、 固有表現抽出・統計指標名には長単位(SudachiDict-C)が有利。 「東京都」が 1 トークンになるかどうかが後段の集計結果を直接変える。
🎯 このコードでやること:指標名 109 個を Janome で分割し(記号は除く)、 1 つの指標名が何語になるか、 どの語が多くの指標名に出てくるか、 1 文字の語がいくつあるかを数える。
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 個で最多)。 最も多くの指標名に出てくる語は「数」の 61 個で、 「数」で終わる 50 個のうち 46 個で「数」が独立の語になる。 1 文字の語は 29 種類あり、 「数」「者」「人」のような接尾的な語が単独のトークンとして大量に出る。 このまま語の出現回数を数えると、 意味の薄い「数」「の」「者」が上位を占めるので、 集計の前に分野別のストップワードで落とすか、 指標名をまとめて 1 語として扱う設計にする。 🖼 の図 2(トークンの文字数)と図 3(分野別のトークン数)は、 同じ分割結果を図にしたものである。
シーン 1:地域経済の自動レポート生成 ── 都道府県の経済政策レポートを自動生成するシステムを考える。 SSDSE-B-2026 の A4103_合計特殊出生率、 C3301_着工建築物数、 I5102_一般診療所数 を引いて文を作る場面で、 列名の単語分割が雑だと「合計・特殊・出生・率」と読まれて「合計特殊出生率」を別物として扱う失敗が起きる。 専門語を保持する SudachiDict-C を使い、 ユーザ辞書に「合計特殊出生率」「着工建築物数」を登録するだけで、 出力文の自然さが格段に上がる。
シーン 2:観光統計の検索インデックス ── 地方自治体の観光振興課が、 観光関連の統計を一括検索したい場面。 SSDSE-B-2026 の G7101_延べ宿泊者数 系列を含めた多数の列を、 単語分割を介してインデックス化する。 「宿泊者」と「延べ宿泊者数」が独立トークンとして登録されないと、 「宿泊者」で検索したときに「延べ宿泊者数」がヒットしなくなる。 これは検索系では致命的で、 SudachiDict の A モード(細粒度)を併用し、 両方のトークンを保持させるのが定石。
シーン 3:自由記述アンケートの感情分析 ── 「住み続けたい理由」を自由記述で収集するアンケートを、 ポジネガ分類する場面。 単語分割の精度が低いと、 否定語「ない」が動詞・形容詞の活用末尾と切れず、 「便利でない」が「便利」と誤判定される。 IPAdic と品詞情報の組み合わせで「否定」を明示的に抽出し、 BERT 系のセンチメント分類モデルへの入力品質を上げるのが王道。
これら 3 シーンに共通する教訓は、 「単語分割は分析の品質を左から支える基礎工事であり、 工事の手抜きは見えない場所で後段の精度を蝕む」という点だ。 早い段階で良い辞書とパイプラインに投資することが、 結局はもっとも安く高品質な分析につながる。
re.sub(r'\s+', '', s) で空白除去後に分割。🎯 このコードでやること:指標名 109 個に NFKC 正規化をかけて、 文字が変わるものと、 Janome の切れ目が変わるものを数える。 手入力の資料で起きやすい全角数字・半角カナの例も並べる。
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)) |
💬 109 個のうち 46 個が NFKC で変わり、 変わる文字は全角の括弧「(」「)」と波ダッシュ「~」の 3 種類だけ。 ほとんどは括弧が半角になるだけだが、 括弧が 2 組続く 6 個(「転入者数(日本人移動者)(男)」など)では、 半角になった「)(」が 1 つの記号としてまとまり、 切れ目の数が変わる。 正規化は分割の結果そのものを変えることがあるので、 正規化してから分割する順番を固定し、 比べる資料の両方に同じ処理をかける。 全角数字の「15」や半角カナの「コンビニ」も、 そろえないと別の語として数えられる。
「単語分割」を中心に置き、 上流・並列・下流の概念を SVG で可視化する。 NLP パイプラインの中での単語分割の役割が一目で分かる。
矢印は「前提 → 単語分割」と「単語分割 → 後続処理」の関係を示す。 辞書ベースとサブワードは並列的選択肢、 形態素解析・単語埋め込み・検索/分類は下流の応用。
| ノード | 具体例 | 関係 |
|---|---|---|
| 自然言語処理 (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 LM | LLM 用トークナイザ |
| ツール | 辞書 | 特徴 |
|---|---|---|
| MeCab | IPADic / UniDic / NEologd | C++ 高速。 デファクト標準 |
| Janome | IPADic 内蔵 | 純 Python・インストール容易 |
| SudachiPy | Sudachi 辞書(多粒度) | 3 種類の分割モード(A/B/C) |
| JUMAN++ | 京大独自 | RNN 言語モデル併用 |
| Kuromoji | IPADic 系 | Java 系・Lucene/Solr で標準 |
| fugashi | MeCab ラッパー | 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 速度」「環境」で決まる。
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・単語埋め込み・分類器と、 下流のすべてに波及します。
| 系統 | 発想の核 | 未知語への態度 |
|---|---|---|
| 辞書ベース | 「知っている語」を並べて最良の敷き詰めを探す | 辞書に無い=原則見えない(別途救済が要る) |
| 統計(HMM/CRF) | 文脈から境界の確率を学習で推定 | 文字素性で「知らない語」も推定できる |
| ニューラル/サブワード | 語をさらに小さい断片に割り、 断片は必ず既知にする | 断片化で未知語を原理的に消す |
辞書ベースは「知っている語で敷き詰めるパズル」、 統計は「境界に賭ける確率」、 サブワードは「そもそも未知語が生じないよう単位を小さくする」。 同じ問題への 3 つの思想だと押さえると、 ツール選択の軸がぶれません。
単語分割で最も痛い誤解は「正しい分割が 1 つある」という思い込みです。 実際は目的・辞書・粒度の選び方で「正解」が動くため、 評価も再現も難しくなります。 既出の「よくある落とし穴」を踏まえ、 分割固有の罠を体系立てて深掘りします。
辞書に無い語は、 辞書ベースだと1 文字ずつバラバラに落ちがちです。 架空例(説明用):新商品名「ソラミドリ茶」が未知語だと ソ / ラ / ミ / ド / リ / 茶 と砕け、 頻度集計でも検索でも消えてしまう。 対策は (a) ユーザー辞書に追加、 (b) 統計モデル(CRF)の文字素性で救済、 (c) サブワード化。 SSDSE-B の列名は「総人口(A1101)」「日本人人口(A1102)」「出生数(A4101)」のように複合語の造語で、 辞書世代によっては「延べ宿泊者数」が 1 語にも「延べ/宿泊/者数」にも割れます。
境界の引き方で意味が変わる典型(架空の説明例):「うらにわにはにわにわとりがいる」、 「ここではきものをぬぐ」(「ここで/履物を/脱ぐ」か「ここでは/着物を/脱ぐ」か)。 人間は文脈で解けても、 辞書+コスト最小だけではもっともらしい方に倒れるだけで、 常に正解とは限りません。 曖昧性は「分割は言語理解と地続き」であることの証拠です。
「単語」の定義自体が揺れます。 UniDic の短単位は「東京/都」、 長単位や NEologd は「東京都」を 1 語。 Sudachi は A(細)/B(中)/C(粗)と粒度を切替可能。 検索では細かい方が再現率、 分類では粗い方が意味のまとまりに有利、 と下流タスクで最適粒度が違う。 「どの単位が正しいか」ではなく「この目的にどの単位が合うか」で選ぶのが正解です。
| 罠 | 症状(架空例で説明) | 対策 |
|---|---|---|
| 辞書世代差 | IPADIC と NEologd で結果が変わり再現不能 | 辞書名・版を固定して明記(例 mecab-ipadic 2.7.0) |
| ドメイン不適合 | 医療・法律・自治体語が一般辞書で砕ける | ドメイン辞書追加・追加学習 |
| 表記ゆれ | 「サーバ/サーバー」「1400/1400」が別トークンに | NFKC 正規化を分割の前段に必ず入れる |
分割の評価は境界 F 値(正解境界と予測境界の一致)や単語 F 値で測りますが、 (a) そもそも「正解分割(ゴールド)」が辞書規約に依存し、 (b) 短単位で作った正解を長単位の出力で測ると不当に低く出ます。 「🧮 実データで計算してみる」の単語一致率 1/3 は、 まさに正解と予測の分割規約がずれると精度が崩れることの最小実例です。 評価するときは「どの規約のゴールドか」「境界単位か単語単位か」を先に固定するのが鉄則です。
単語分割の手法は「ルール/統計/ニューラル」の順に発展し、 いまは形態素解析器(MeCab/Sudachi)とサブワード(BPE/SentencePiece)の併用が実務標準です。 それぞれの原理と勘所を地図として整理します。
最長一致法(greedy longest-match)は「その位置から始まる辞書語のうち一番長いものを貪欲に選ぶ」素朴な方法。 高速ですが局所最適で誤りやすい。 これを大域最適化したのがコスト最小法(ラティス+ Viterbi)で、 文の全分割候補をグラフ(ラティス)に展開し、 「単語コスト+連接コスト」の総和が最小の経路を動的計画法で選びます。 MeCab の中核はこれで、 「📐 定義 / 数式」の $\arg\max P(W\mid S)$ を対数コスト最小化として解いています。 下の「🎮 触って理解する」で最長一致とコスト最小の差を手で体験できます。
HMMは「単語列を隠れ状態、 文字列を観測」とみなし、 遷移確率と出力確率の積を最大化。 CRF(条件付き確率場)は各文字に「単語の先頭/中間/末尾(BIES など)」ラベルを付ける系列ラベリングとして分割を解き、 周囲の文字を素性として使えるため未知語に強い。 現代 MeCab の学習も CRF ベースで、 「辞書+統計」のハイブリッドになっています。
BiLSTM-CRF や BERT ベースの文字レベル分類器は、 文脈埋め込みで境界を推定します。 精度は高い一方、 モデルが重く辞書の即時更新が効きにくい。 実務では「速度・更新性が要る前処理は MeCab/Sudachi、 精度が要る解析はニューラル」と使い分けるのが定番です。
LLM 時代の主役。 発想は「単語より小さい断片に割り、 断片語彙を有限に固定すれば未知語が原理的に消える」。 「🧮 実データで計算してみる」の BPE 手計算(海道 をマージ)がまさにこの原理です。 三者の違いを一言で:
| 手法 | マージ/分割の基準 | 代表 |
|---|---|---|
| BPE | 最頻の隣接ペアを貪欲にマージ | GPT 系 |
| WordPiece | 尤度(言語モデル的スコア)が最大化するペアをマージ | BERT |
| Unigram LM | 大きな語彙から確率の低い断片を削っていく | SentencePiece(既定) |
SentencePiece は空白も 1 記号(▁)として扱い、 生テキストから直接学習できるため言語非依存。 だから日本語でも「事前分かち書き不要」で回せます。
分割は前処理なので、 その良し悪しは静かに全下流へ伝播します。 TF-IDF / N-gram:トークンの粒度が語彙サイズと頻度分布を直接決める。 単語埋め込み:分割単位が語彙になるので、 粒度がベクトル空間の解像度を決める。 検索:登録トークンの粒度が再現率と適合率のトレードオフを左右する。 分類・要約:意味のまとまりが粗すぎても細かすぎても性能が落ちる。 「分割は独立した工程」ではなく下流と一体で最適化する対象だと捉えるのが上級者の視点です。
単語分割は NLP 前処理の入口です。 ここで掴んだ「境界を選ぶ」「粒度で下流が変わる」という感覚は、 次の用語群でそのまま効いてきます。 用語集内の実在ページへ辿って理解を広げてください。
補足:BERT・トークナイザ・SentencePiece など単体ページが用語集に無いものは、 上記「形態素解析」「埋め込み」「NLP」内で触れられています。
日本語は空白が無いので、 まず「どこで区切るか」を決めねばなりません。 最も素朴な方法が 最長一致法(greedy longest-match)です。 左から順に、 その位置から始まる辞書語のうち一番長いものを貪欲に選び、 選んだぶんだけ右へ進む —— これを繰り返すだけ。 ここでは実際の解析器は使わず、 教材用に手で固定した小さな辞書(下に全語を明記)を用いた決定的(毎回同じ結果)なデモで、 (a) 1 文字ずつ最長一致を探す過程、 (b) 貪欲が間違える例とコスト最小(語数最小)による正しい分割との対比、 (c) 辞書に無い語(未知語)の扱い、 を体感します。 例文はすべて架空の固定例です。
※ 実際の辞書(IPADIC 約 35 万語, UniDic 約 75 万語)とは無関係の、 デモ専用の極小辞書です。 語彙をわざと絞ることで「辞書に無い語=未知語」の挙動も観察できます。
例文を選び、 スライダー(またはボタン)を動かすと、 左端から 位置ごとに「その場所から始まる辞書語」を長さ 1・2・3 と調べ、 一番長いものを選ぶ 様子が 1 手ずつ見えます。 緑=確定済み、 太線=今まさに選ばれた最長語、 赤=辞書に無く未知語として 1 文字だけ進む場合です。
最長一致は「今この場で一番長い語」を選ぶだけなので、 先を見ていない。 その結果、 最初に長い語を取ったせいで残りがバラバラになることがあります。 一方 語数が最小になる分割を全体最適で探すと(動的計画法。 実装は決定的)、 より自然な区切りが得られることがあります。 下は選択中の例文での両者の比較です。
※ これは簡易デモです。 本デモの辞書・分割結果は教材用に手で固定した値であり、 特定ツールの出力を再現するものではありません。 実運用の解析器は数十万語規模の辞書と、 学習で推定した連接コスト表(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 モデル、 上位の枠組み 自然言語処理 と合わせて読むと、 「切る → つなぎを確率で見る → モデル化」の流れがつながります。