固有名詞は辞書で引く。「はるばる」が女将を呼んだ
Proper nouns come from a dictionary: how "harubaru" summoned the landlady
D05-4-T10-gazetteer-entities・技術・日本語入力でプレイする仕組み
港の船長に「はるばる札幌から来ました」と入力すると、船長が「番屋を出た時刻の食い違い、そういう話か?」と聞き返してきます。
「はるばる」の「はる」が、食堂の女将ハルの名前として辞書に当たったからです。
この回は、その辞書の作りと、名前が当たったときの加点の決まりを書きます。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 8 回目)
先に読むとよい回: T03(文字の正規化)・T09(RRF を実装して測り負けた話)
次に読む回: T11(日付・時刻・時間帯)
名前 — gazetteer#
- 正式名
- gazetteer(ガゼッティア)。辞書で引く固有表現抽出は dictionary-based named entity recognition とも呼ぶ
- 略称
- なし(NER は named entity recognition の略)
- 日本語
- 固有表現辞書、地名辞典(もとの意味)
- 初出
- 語としての初出は確かめられませんでした。NER で gazetteer の要否を正面から測った古典に Mikheev, Moens, Grover, "Named Entity Recognition without Gazetteers"(EACL 1999)があり、ニューラル NER に辞書を足す近年の例に Song, Lawrie, Finin, Mayfield, "Improving Neural Named Entity Recognition with Gazetteers"(arXiv 2020)があります。辞書の語を一度に引く定番のアルゴリズムは Aho & Corasick, "Efficient String Matching: An Aid to Bibliographic Search"(CACM 1975)です。
gazetteer とは#
もとは地名辞典のことです。
NER では、人名・地名・組織名などの「名前の一覧」を指します。
文の中で一覧の語が現れた所を拾い、その語の種類(人・場所・組織)を付けます。
ふつうの使われ方は二つあります。
一つは、辞書の一致そのものを答えにするやり方です。
語の一覧を Aho–Corasick のような多パターン照合の機械にして、文を 1 回なめて全部の出現を取ります。
重なりは「最長一致」で解くのが普通です。
「岬の高台」と「岬」が両方当たったら、長い方を採ります。
もう一つは、辞書の一致を特徴量にして、統計モデルに渡すやり方です。
CRF の時代には「この語は人名辞書にある」が強い特徴でした。
ニューラルの時代にも、辞書の一致を入力に足すと、辞書にある珍しい名前で効く、と報告されています(上の 2020 年の論文)。
よく知られた弱点は四つです。
- 曖昧さ。同じ綴りが人名にも地名にもなる。英語なら Washington、日本語なら「岬」。
- 網羅。辞書に無い名前は一つも取れない。別名・略称・誤記を全部書くことはできない。
- 短い語の誤爆。2 文字の名前は、普通の語の中に部分として現れる。
- 文脈を見ない。「〜さん、」と呼びかけた名前も、話題にした名前も、同じ一致になる。
1999 年の Mikheev らは、MUC-7 のデータで辞書の大きさと種類を変えて測り、「よく知られた名前の小さな辞書」でもほぼ同じくらい取れる、と書いています。
大きな辞書を足すより、文脈の規則の方が効く場面がある、という結果です。
うちの辞書の形#
作品フォルダの entities.json です。
第一章の人物・場所・一般語から、4 件だけ写します。
{ "id": "hal", "aliases": ["ハル", "女将", "おかみ", "ハルさん"], "person": true },
{ "id": "domoto", "aliases": ["堂本", "巡査", "駐在", "どうもと"], "person": true },
{ "id": "banya", "aliases": ["番屋", "食堂", "店"] },
{ "id": "alibi", "aliases": ["アリバイ", "昨日の夜", "昨日の晩", "昨晩", "夕べ", "事件の夜", "あの晩", "どこにいた", "居場所", "所在", "足取り", …] }
今日数えると、対象は 61、別名は 453 です(2026-10-29 時点)。
person(人物)が 11、proper(聞いて初めて知る名前)が 12、revealedBy(このフラグが立つまで世界がその名を口にしてはいけない)が 2。
意図 248 のうち 155 が、自分に関係する対象を entities で持っています。
人物の別名に肩書き(女将・巡査・駐在)を入れているのは、刑事が名前より肩書きで呼ぶことが多いからです。
alibi は名前ですらありません。
「あの晩」「どこにいた」という、居場所を聞くときの言い回しを一つの対象にまとめています。
引き方#
起動時に、別名を全部 norm/1 で正規化して持ちます(正規化の中身は T03 の回)。AdvMatcher.hpp の build から写します。
EntityNorm.clear();
for (const auto& [Id, Aliases] : InEntities.aliases)
{
for (const std::string& A : Aliases)
{
EntityNorm[Id].push_back(NormalizeText(A));
}
}
照合のときは、入力も同じ正規化をかけ、別名が部分列として含まれるかを見ます。
static bool ContainsSub(const std::vector<char32_t>& Hay, const std::vector<char32_t>& Needle)
{
if (Needle.size() > Hay.size()) { return false; }
for (size_t i = 0; i + Needle.size() <= Hay.size(); ++i)
{
bool bOk = true;
for (size_t j = 0; j < Needle.size(); ++j)
{
if (Hay[i + j] != Needle[j]) { bOk = false; break; }
}
if (bOk) { return true; }
}
return false;
}
Aho–Corasick ではなく、素朴な二重ループです。
入力は 1 文、別名は 453 なので、1 手あたりの比較はたかが知れています。
正規化のおかげで、カタカナとひらがな、全角と半角、長音の有無は同じ語になります。
「ハールさんの店にいたのか」は「はるさんの店にいたのか」になり、hal と banya に当たりました。
入力の解析(AdvInput.hpp)も同じ辞書を引きますが、こちらは出現の位置を全部返します。
「岬の高台には誰がいましたか」は、hilltop(岬の高台)、misaki(岬)、hilltop(高台)の 3 件が重なったまま出ます。
最長一致で解いていません。
後段の主張の判定(T13 の回)が、この位置を使います。
加点の式#
意図ごとの加点は、その意図が持つ対象のうち、入力に当たったものの数で決まります。
auto BonusFor = [&](const Intent& It) -> double
{
int Hits = 0;
for (const std::string& Ent : It.entities)
{
const auto Found = EntityNorm.find(Ent);
if (Found == EntityNorm.end()) { continue; }
for (const std::vector<char32_t>& Alias : Found->second)
{
if (!Alias.empty() && ContainsSub(Norm, Alias)) { ++Hits; break; }
}
if (Hits >= 2) { break; }
}
return Th.entityBonus * Hits;
};
entityBonus は 0.15。
一つの対象は、別名がいくつ当たっても 1 回しか数えません(break)。
対象は 2 つまでで打ち切るので、加点の上限は 0.30 です。
並びは意図の entities に書いた順で、3 つ目以降は見ません。
意図の点は、表層と埋め込みを混ぜた値(0〜1)にこの加点を足したものです。
だから 1 を超えます。
駐在所で「検分の結果を教えてください」と入力すると、言い換えとほぼ同じ文なので混ぜた値は 1 に近く、検分の加点が乗って 1.150 で採用されました。
加点はもう一つ、段の選択に使います。
同じ話題の中で、「材料を挙げたときだけ入れる段」があります(段の話は T19 の回)。
港で佐伯に「2月15日の夜はどこにいましたか」と入力すると、ログにはこう出ます。
stage 突きつける材料(alibi)を挙げたので通した
点に足す加点と、段を開ける材料の判定は、同じ hasEntity の一致です。
既読の段は、加点で追い越せない#
加点には副作用があります。
同じ名前を何度も入力すると、一度聞いた話が加点で勝ち続けます。
そこで、既に再生した段に着地する候補からは、加点を打ち消します。
Cd.bSeen = ResolvesToSeenStage(*It);
if (Cd.bSeen) { Cd.RepeatPenalty = std::min(Th.repeatMargin, Cd.Bonus); Cd.Penalty += Cd.RepeatPenalty; }
Cd.Score = Cd.Cos + Cd.Bonus - Cd.Penalty;
repeatMargin は 0.3、つまり加点 2 つぶんです。
一律に引くのではなく、加点の分だけを戻すのが要点です。
意味(混ぜた値)で勝っている既読の候補は、そのまま勝ちます。
一律に 0.15 を引いたら、言い直した人に別の話を返す事故が出た、と設定ファイルの注記に残っています。
今日のログです。
港で佐伯に「あの晩、番屋にいたんですか」と入力すると、1 回目は「あの晩」と「番屋」が当たって加点 0.30、混ぜた値 0.500 と合わせて 0.800 で採用されます。
同じ文をもう一度入力すると、その候補は既読なので 0.300 が打ち消されて 0.500。
加点 0.15 の別の候補が 0.604 で上に来て、そちらが返りました。
この打ち消しは順位にだけ効き、「答えてよいか」の判定には効きません(順位と判定を分けた話は T09 の回)。
競う相手がいなければ、二度目でも言い直しを返します。
呼びかけと、目的語の例外#
名前の一致を、告発の名指しを数えるのにも使っています。
推理の段で別人の名前を挙げた見立ては、候補から外します。
ここに例外が二つあります。
一つは呼びかけです。
「堂本さん、これは殺しだ」の「堂本」は聞き手で、堂本を犯人と言っているのではありません。
正規化の norm/1 は読点を落とすので、この判定にだけ読点を残した列を作り、文頭が「〈名前〉さん/くん+読点」で、後ろに同じ名が出てこなければ呼びかけと見なします。
もう一つは目的語です。
「を殺/を襲/を殴」の前にだけ出てくる名前は、告発の相手に数えません。
どちらも、一般の構文解析ではない、限定した表層規則です。
名前どおりでない所 — gazetteer#
gazetteer の変形です。三つ違います。
- 辞書に名前でない語が入っている。alibi(「あの晩」「どこにいた」)や、揉め事の言い回しの一覧も同じ entities.json にあります。gazetteer というより、キーワード辞書や概念辞書と呼ぶ方が近い部分です。
- 出力が「種類つきの区間」ではなく「点への加点」と「段の材料」。NER の答えとして名前の区間を返すのが目的ではありません。
- 最長一致で重なりを解いていない。加点は対象ごとに 1 回なので困らないのですが、解析の区間は重なったまま後段へ行きます。
revealedBy と proper(先出しの見張り)は、うちの制作上の印で、gazetteer とは関係ありません。
弱点と、採らなかった案 — gazetteer#
今日入力して出た弱点が二つあります。
一つ目は、冒頭の「はるばる」です。
女将の別名「ハル」は正規化で「はる」になり、「はるばる」の中に部分として現れます。
加点 0.15 で、佐伯の「番屋を出た時刻」の意図が 0.295 まで上がり、聞き返しの帯(0.27〜0.33)に入りました。
採用はされず、聞き返しで止まります。
ただ、刑事が自己紹介をしただけで、船長が時刻の食い違いの話を持ち出すのは変です。
2 文字の別名の誤爆は、辞書引きの教科書どおりの弱点です。
二つ目は、呼びかけです。
「堂本さん、これは殺しだ」と入力すると、今日は雑談(堂本の勤務の話)が 0.532 で勝ちました。
「これは殺しだ」だけなら、事故の見立てを押す意図が 0.373 で採用されます。
呼びかけの例外は名指しの数え方にしか効かず、埋め込みの入力からは「堂本さん」を外していません。
人名を含む文の埋め込みが、その人の身の上話の言い換えへ寄ったのだと読んでいます。
採らなかった案です。
- 別名を全部、語の境界で切ってから当てる。日本語には空白が無いので、境界を知るには形態素解析が要ります。照合で形態素解析をしない方針(T04 の回)と衝突します。
- 2 文字の別名を外す。「ハル」「佐伯」「番屋」「岬」が消えます。人名は 2 文字が普通です。
- 誤爆する語を除外の一覧にする(「はるばる」「はるか」など)。一つ見つけるたびに足す形になり、足し忘れが起きます。今のところ聞き返しで止まっているので、入れていません。
- 呼びかけを埋め込みの入力から切り取る。切り取り方を誤ると、名指しの推理まで名前を失います。今日の誤着地を見て、次に直すならここだと考えています。
辞書は書いたとおりにしか当たりません。
当たった理由が必ずログに出るのが、辞書のいちばんの取り柄です。
検索語#
gazetteer named entity recognition
dictionary-based NER
Named Entity Recognition without Gazetteers
Aho–Corasick algorithm
港で「はるばる札幌から来ました」と入力すると、船長が少しずれた聞き返しをします。『霧走町 流氷に眠る』の第一章はブラウザで、プロフィールのリンクから入力できます。