1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本語をLLMに載せる前の正規化 — 全角半角・異体字・表記ゆれの実装メモ

1
Posted at

始まりは、社内文書 3000 件の投入テスト

ある日本企業向けの PoC で、社内 FAQ と業務規定 3000 件を LLM に食わせる工程を書いていた。動くには動いた。だが、返ってくる回答が妙に取りこぼす。「株式会社◯◯」で通るのに「(株)◯◯」だと引かない。「サーバー」で引けるのに「サーバ」だと空振り。原因は明白で、前処理が甘かった。

正直、日本語の前処理を舐めていた。英語の感覚で lower() と空白詰めくらいで済むと思っていたのだが、日本語は入力段でもっと崩れている。全角と半角、異体字、カタカナの長音、丸数字や単位記号。これらを LLM に投げる前に潰しておかないと、埋め込み検索も RAG も生成も、すべて微妙に外す。

日本語をLLMに載せる前の正規化 — 全角半角・異体字・表記ゆれの実装メモ

前提と使うもの

  • Python 3.11

  • unicodedata (標準ライブラリ) / mojimoji / 軽い独自辞書

  • 用途: LLM への入力(検索クエリ・埋め込み対象・プロンプト同梱テキスト)の正規化

先に断っておく。正規化は「かけすぎるとむしろ意味が壊れる」。後半でその線引きを書く。

1. NFKC — まず素直に一本入れる

迷ったら NFKC を先に一発通す。全角英数字、半角カナ、丸数字、単位記号、㈱ の類までまとめて素直な文字に寄る。Python なら標準ライブラリだけで済む。

import unicodedata
def normalize_nfkc(text: str) -> str:
return unicodedata.normalize("NFKC", text)
print(normalize_nfkc("ABC123 アイウエオ ①②③ ㈱㌖"))
→ ABC123 アイウエオ 123 (株)キロメートル

ここまでで、たとえば「サーバー1台」と「サーバー1台」が同じキーで比較できる。素朴だが、これを入れていない社内システムがまだ本当に多い。

2. 全角半角のうち「英数だけ」を寄せたい時

NFKC は乱暴に言うと全部持っていく。だが「カタカナは全角のまま残したい」「英数と記号だけ半角に寄せたい」という要件がわりとある。日本語は全角カナのほうが可読性が高く、検索精度が落ちない事も多い。

ここで mojimoji の細粒度が効く。

import mojimoji
s = "サーバー1台導入"
半角カナ・半角英数をすべて全角に
print(mojimoji.han_to_zen(s))
→ サーバー1台導入
カタカナだけ全角に、英数はそのまま半角
print(mojimoji.han_to_zen(s, digit=False, ascii=False))
→ サーバー1台導入

実運用では「NFKC で一旦寄せて、そのあとカタカナだけ全角に戻す」二段構えにしていることが多い。

3. 異体字問題 — 髙・﨑・辻の "はしご" は消せない

ここが日本語独特の罠だ。「髙橋」の "はしご高"、「﨑」の "たつさき"、「辻」の点の数。これらは NFKC ではほとんど寄らない (一部の CJK 互換漢字は寄るが、"はしご高" のような字形バリアントは寄らない)。

で、実装で厄介なのは「寄せて良いのか」の判断だ。人名や社名は勝手に "高橋" に書き換えると失礼どころか、誤同定になる。だが、社内文書の検索キーとしては同一人物と扱いたい。答えは、「原文は保持しつつ、検索キーだけ別に持つ」構造にするしかない。

VARIANT_MAP = {
"髙": "高", "﨑": "崎", "德": "徳",
"眞": "真", "澤": "沢",
"邊": "辺", "邉": "辺",
}
def fold_variants(text: str) -> str:
for k, v in VARIANT_MAP.items():
text = text.replace(k, v)
return text
保存は原文 / 検索は fold_variants(nfkc(text)) を index に

辞書は業界・会社ごとに追記していく。網羅を諦め、頻出だけ抑えるほうが実用的だ。全 CJK 互換漢字を機械的に寄せる方法もあるが、副作用が読めない。

4. カタカナ表記ゆれ — 長音とヴィ/ヴェの闇

「サーバ」/「サーバー」、「コンピュータ」/「コンピューター」、「インターフェース」/「インターフェイス」。JIS 規格の指針(音引きを付ける/付けない)は複数の流派があり、社内文書の中でも書き手ごとにブレる。

ここは辞書というより変換ルールで攻める。

import re
def fold_katakana(text: str) -> str:
# 末尾のカタカナ長音を落とす(検索キー用)
text = re.sub(r"([ァ-ヴ])ー(?=[^ァ-ヴー]|$)", r"\1", text)
# ヴィ→ビ、ヴェ→ベ、ヴァ→バ、ヴォ→ボ
for a, b in [("ヴィ","ビ"), ("ヴェ","ベ"), ("ヴァ","バ"), ("ヴォ","ボ"), ("ヴ","ブ")]:
text = text.replace(a, b)
return text

これも「原文は残す・検索キーだけ寄せる」の思想で組む。生成に混ぜてはいけない — LLM に "ヴィヴィアン" を "ビビアン" として渡すと、固有名詞の同定に失敗する。

5. 数字と単位の混在

「3,000円」/「3000円」/「3千円」/「三千円」。この揺れは NFKC では取れない。金額や個数を検索・集計するなら、数字列を独立に抽出して正規化する。

KANJI_NUM = {"零":0,"一":1,"二":2,"三":3,"四":4,"五":5,
"六":6,"七":7,"八":8,"九":9,
"十":10,"百":100,"千":1000,"万":10000}
def kanji_to_int(s: str) -> int:
# 簡易版 — "三千五百" 程度まで
total, current = 0, 0
for c in s:
n = KANJI_NUM.get(c, 0)
if n >= 10:
current = (current or 1) * n
total += current
current = 0
else:
current = current * 10 + n
return total + current
数値表現の抽出は別途 regex で(1234, 5,000, 3千 など)

ここまで来ると "前処理" というより "意味解析" の入口だが、業務データではよく必要になる。

動作確認 — 4 パターンを同一キーに寄せる

samples = [
"㈱テスト商事 サーバー1台 3,000円",
"(株)テスト商事 サーバ1台 3000円",
"㈱テスト商事 サーバー壱台 三千円",
"(株)テスト商事 サーバー 1 台 3,000 円",
]
def key(text: str) -> str:
text = normalize_nfkc(text)
text = fold_variants(text)
text = fold_katakana(text)
text = re.sub(r"\s+", "", text)
return text
for s in samples:
print(key(s))

上 3 パターンはだいたい同一キーに寄る。4 番目の "壱台" と "三千円" は数値正規化まで積まないと寄らない。ここが線引きで、「どこまでを検索キーの一致に含めるか」は業務要件次第。ふつうは "壱/一" を潰す前で止める(誤集計リスクのほうが大きい)。

正規化しすぎて壊れる境界

正規化は片方向で不可逆な作業だ。だから、原文を残さないと後戻りできない。実装した後で気づいた "やってはいけない" を並べておく。

  • 生成に渡す本文まで正規化しない — LLM に "顧客名: 高橋" と渡してしまうと、返信で "髙橋様" と書きたい場面で使えない。正規化は index / 検索キーだけに閉じる

  • 敬称・敬語まで潰さない — "様" を "さん" に寄せるような処理を軽率に入れると、日本語のトーンが崩壊する

  • 固有名詞辞書を先に持たない — 顧客リスト・製品名の辞書がある業務なら、そちらを優先し、機械的な正規化はそのあと

結局のところ、日本語の前処理は「入力を潰す」より「入力の別レイヤーを作る」設計になる。原文はそのまま、検索用キーは別、生成用テキストはさらに別。3 レイヤー持つのが実装として一番素直だった。

応用・発展

この基盤ができると、上に何を乗せるかで用途が広がる。

  • RAG の埋め込みインデックス作成前段 — 揺れに強いドキュメントストアになる

  • 社内 FAQ の重複検出 — 同一質問の別表記を束ねる

  • 問い合わせフォームの名寄せ — 顧客識別の精度が上がる

ちなみに次回は、この正規化の "手前" にあるもっと厄介な話 — FAX・スキャン PDF・レイアウトが崩れた文書を、そもそも LLM に載せられる形に持ってくる話を書く予定。日本企業の文書は Word や Markdown ではなく、紙と PDF が主戦場だ。正規化以前に、テキスト抽出そのものが罠になる。

まとめ

NFKC 一発で 8 割は片付く。残り 2 割 — 異体字、カタカナの揺れ、数字表現 — は業務ドメインごとに手当てするしかない。そして正規化は「原文を残す」設計と表裏一体で、ここを間違えると後段で LLM の応答品質が下がる。

筆者は 5years+ で日本市場向けの LLM 業務応用を担当している。同じ場所で 30 分ハマる開発者が少しでも減れば、この記事の役割は終わりだ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?