「事故ではない」と「事故ではない根拠」は、否定の掛かり先が違う
"Not an accident" and "the grounds that it was not an accident" negate different things
D07-3-T13-negation-polarity・技術・日本語入力でプレイする仕組み
駐在に「これは事故ではない」と入力するのと、「事故ではない根拠を聞かせてください」と入力するのとでは、否定の働きが違います。
前者は「事故だ」という言い分を打ち消し、後者の否定は「根拠」という名詞に掛かっているだけです。
この回は、その違いを入力のどこで見て、何に使っているかを書きます。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 11 回目)
先に読むとよい回: T03(文字の正規化)・T12(複文を節に割る)
次に読む回: T14(指示語を画面から探す)
名前 — negation scope resolution#
- 正式名
- negation scope resolution(否定のスコープの解決)
- 関連
- negation detection(否定の検出)、その代表的な規則の方法 NegEx
- 略称
- なし(NegEx は方法の名前)
- 日本語
- 否定のスコープ解決、否定表現の検出、極性(polarity)の判定
- 初出・代表
- Morante & Blanco, "*SEM 2012 Shared Task: Resolving the Scope and Focus of Negation"(*SEM 2012)。規則の側は Chapman, Bridewell, Hanbury, Cooper, Buchanan, "A Simple Algorithm for Identifying Negated Findings and Diseases in Discharge Summaries"(Journal of Biomedical Informatics, 2001)
否定のスコープとは#
否定の手掛かり(not、never、ない、ではない)を見つけるのが、否定の検出です。
その手掛かりが、文のどこまでを打ち消しているかを決めるのが、スコープの解決です。
*SEM 2012 の共通課題では、スコープを「否定される意味の部分」、フォーカスを「スコープの中で最も明示的に否定される部分」と定義しています。
文法書(Huddleston & Pullum 2002)の用語を借りたものです。
例で書きます。
「彼は昨日、港に行かなかった」のスコープは「彼が昨日港に行った」全体ですが、フォーカスは文脈によって「昨日」かもしれないし「港」かもしれません。
「昨日ではなく今日行った」なら、フォーカスは「昨日」です。
NegEx は、医療の退院サマリーで「所見が無い」と書かれた病名を見つける、正規表現の方法です。
「no」「denies」「without」などの否定の語の一覧を持ち、見かけだけ否定の語(「no increase」のような偽の否定)を除き、否定の語から一定の範囲の中だけを否定されたと見なします(スコープを限る)。
論文の評価は、特異度 94.5%、陽性的中率 84.5%、感度 77.8% でした。
語の一覧と窓だけで、かなりの所まで行く、というのが要点です。
統計やニューラルの方法は、スコープを語ごとの系列ラベルとして当てます。
構文木の上でスコープを決める規則の方法もあります。
よく知られた弱点は三つです。
- 偽の否定。「〜だけじゃない」「〜のではないか」は否定の形ですが、打ち消していません。
- スコープの境界。否定が名詞を修飾するだけのとき(a not unreasonable request、日本語なら「事故ではない根拠」)は、文の主張を打ち消しません。
- 二重否定と反語。「〜ないとは言えない」は、ほぼ肯定です。
うちでは、照合は否定を見ない#
先に、照合の側の事情を書きます。
駐在で今日入力した結果です。
「これは事故ではない」→ 事故の見立てを押す意図に 0.643 で着地
「これは事故だ」→ 同じ意図に 0.523
「やはり事故でしょう」→ 同じ意図に 0.669
埋め込みは、肯定と否定をほぼ同じ所に置きます。
「事故だ」と「事故ではない」は、話題としては同じだからです。
この意図は、どちらの言い方でも堂本が「現場を歩いた刑事さんがどう見るか、聞きたい」と返す作りなので、それで困りません。
困るのは、極性で結論が逆になる推理です。
照合の注記にはこう書いてあります。
// excludes: 主張が反転する語を含む入力では、その意図を候補から外す。
// 減点ではなく除外にするのは、埋め込みが肯定と否定をほぼ同じ距離に置くため——
// どれだけ引いても、言い方次第で戻ってきてしまう(08/05/26 W-3)。
点数をいじっても否定は直らない。
だから、否定は点数の外で、入力の原文を見て決めます。
それが入力の解析(AdvInput.hpp)の「極性の反転」と、主張の判定(AdvClaims.hpp)です。
否定の手掛かりの一覧#
共通の言語資源 input.json に置いています。
"polarityReversers": ["とは言えない", "とはいえない", "わけではない", "わけじゃない", "とは限らない", "とはかぎらない", "ではない", "じゃない", "ではありません", "じゃありません", "ではなかった", "じゃなかった", "とは思えない", "はずがない"],
"rhetoricalNegations": ["だけじゃない", "だけではない", "だけでない", "だけじゃなく", "だけではなく", "のではないか", "のではないでしょうか", "のではありませんか", "んじゃないか", "んじゃないですか", "んじゃないでしょうか", "じゃないですか", "ではないですか", "じゃないか", "ではないか", "ではないでしょうか"],
"attributiveNegationBeforeNominal": true,
反転の語は 14、偽の否定(念押し・追加)は 16 です。
エンジンの C++ には語を一つも書いていません。
取り出し方#
AnalyzeInput の該当部分を写します。
// ---- 極性の反転 ---------------------------------------------------------------
// 否定の形をした問いかけ(〜のではないでしょうか)は反転に数えない
std::vector<InputSpan> Rhetorical;
for (const std::string& W : L.rhetoricalNegations)
{
for (const InputSpan& Sp : FindWord(Original, S, Norm, W)) { Rhetorical.push_back(Sp); }
}
for (const std::string& W : L.polarityReversers)
{
for (const InputSpan& Sp : FindWord(Original, S, Norm, W))
{
bool bRhet = false;
for (const InputSpan& R : Rhetorical) { if (R.overlaps(Sp)) { bRhet = true; break; } }
if (bRhet) { continue; }
// 名詞を修飾する否定: すぐ後ろが漢字・カタカナなら、否定は後ろの名詞に掛かっていて、
// 文の主張を反転させない。文字の種類は Unicode の区分(言語の語彙ではない)
if (L.attributiveNegationBeforeNominal && Sp.endByte < Original.size())
{
const std::vector<char32_t> Next = DecodeUtf8ToU32(Original.substr(Sp.endByte, 4));
const char32_t C = Next.empty() ? 0 : Next[0];
const bool bNominal = (C >= 0x4E00 && C <= 0x9FFF) || (C >= 0x3400 && C <= 0x4DBF) || (C >= 0x30A1 && C <= 0x30FA);
if (bNominal) { continue; }
}
bool bDup = false;
for (const InputSpan& R : A.reversals) { if (R.overlaps(Sp)) { bDup = true; break; } }
if (!bDup) { A.reversals.push_back(Sp); }
}
}
手順にすると四つです。
- 一覧の語を、正規化した列の上で全部探す。見つかった位置は原文の byte 区間に戻す(正規化の写像は T03 の回)。
- 念押し・追加の語と重なる否定は捨てる。
- 否定のすぐ後ろの 1 文字が漢字(CJK 統合漢字と拡張 A)かカタカナなら、名詞を修飾する否定として捨てる。
- 既に取った区間と重なるものは捨てる(「ではなかった」と「ではない」の二重取り)。
3 の判定は、言語の語彙ではなく Unicode の区分です。
使うかどうかを言語資源の真偽値で決めます。
ひらがなの名詞(「ことば」)は名詞と見なしません。
後ろが読点や句点なら、文の否定として残ります(「事故ではない、殺しだ」)。
今日入力した結果です。
- これは事故ではない → 反転 1(ではない)
- 事故ではない根拠を聞かせてください → 反転なし(後ろが「根」)
- ただの事故ではない話 → 反転なし(後ろが「話」)
- これは事故ではないのではないですか → 反転 1。後ろの「ではないですか」は念押しとして捨て、前の「ではない」は残る
- 事故とは言えない/事故じゃなかった/事故だったはずがない → それぞれ反転 1
念押しの文は、質問にもしません。
文末が念押しの形なら、質問の手掛かり(「ですか」)があっても質問の印を外します。
「これは事故ではないのではないですか」は、問いではなく、言い分として扱われます。
引用も、ここで分けています。
「」の直後に「と言」「と聞」「と証言」などが続くか、「と」「って」の後に同じ文の中で「言った」「証言した」などが続けば、その括弧は他人の発言の引用(citation)です。
「「事故だ」と署は言った」の「事故だ」は、刑事の言い分ではなく署の言い分です。
主張の判定で使う#
取り出した否定を使うのは、推理の主張の判定だけです。
主張の形は、単体検査の検査用データが一番短いので、それを写します(作品のデータではありません)。
{ "id": "c.time", "purpose": "deduction", "recordLabel": "時刻の矛盾", "knownWhen": ["f_clock", "!f_done"],
"alternatives": [ { "all": [ { "textAny": ["時計"] }, { "textAny": ["遅れ", "進んで"] } ] } ] }
alternatives の中は OR、all の中は AND、textAny の中は OR です。
既定では、all の全部が同じ文の中に揃わなければいけません(文の区切りは T12 の回)。
揃ったとき、否定をこう見ます。EvaluateClaim から写します。
// 極性の反転(一致の後ろで、一致した語の内側ではないもの)
if (M.result == ClaimResult::Supported)
{
for (const InputSpan& R : A.reversals)
{
if (!Region.contains(R) || R.beginByte < FirstBegin) { continue; }
bool bInside = false;
for (const InputSpan& S : M.spans) { if (S.overlaps(R)) { bInside = true; break; } }
if (!bInside) { M.result = ClaimResult::Ambiguous; M.why = "polarity reversed: " + A.slice(R); break; }
}
}
境界を並べます。
- 同じ文の中で、最初に一致した語より後ろにある否定だけを見る。「時計が遅れていたとは言えない」は、一致の後ろに「とは言えない」があるので、確定しません。
- 一致した語の内側にある否定は見ない。作者が「転倒ではない」のような否定の形の語を textAny に書いたとき、その「ではない」で自分を打ち消さないためです。
- 一致より前の否定は見ない。
- 否定が見つかったら、結果は ambiguous(確定しない)。主張は成り立たず、話は進まず、相手が「何が言いたいのか」を確かめる側へ回ります。
- 引用(citation)の中の語は、一致に数えない。「「時計が遅れていた」と佐伯が言った」は、主張として成り立ちません。括弧だけの「「時計が遅れていた」」は、自分の言葉として数えます。
- 質問の文で揃ったら、interrogative(疑問として挙げた主張)。「はい」と確かめてから実行します。
単体検査は 90 項目、今日すべて通りました。
名前どおりでない所 — negation scope resolution#
negation scope resolution の、ごく限定した表層規則です。
- スコープを区間として求めていません。見ているのは「否定の語が、主張の一致の後ろ、同じ文の中にあるか」だけです。NegEx の「否定の語から一定の範囲だけ」に近い考え方ですが、範囲は文全体です。
- フォーカスは見ません。「昨日ではなく今日」のような、どこが否定されたかは区別しません。
- 名詞の修飾の判定は、後ろの 1 文字の文字種だけです。構文は見ません。
- 否定を見つけても、反対の主張として成り立たせることはしません。「事故ではない」から「殺人だ」を導くのは、作者が書いた主張の語(textAny)の仕事で、否定の検出の仕事ではありません。
計画の段階では「否定で主張を反転させる」と書いていましたが、ソースがしているのは「否定があれば確定させない」です。
本文はソースに合わせました。
弱点と、採らなかった案 — negation scope resolution#
今日、動詞の否定が取れないことを確かめました。
「佐伯は番屋にいなかった」→ 反転なし
一覧にあるのは「ではない」「じゃない」のような名詞・形容動詞に付く否定と、「とは言えない」のような言い回しで、動詞の「〜なかった」「〜ていない」はありません。
動詞の否定を一覧に足すと、「〜しかない」「〜かもしれない」「足りない」のような、否定ではない「ない」が大量に当たります。
いまの主張の語が名詞の言い切り(「転倒ではない」の形)中心に書かれているので、そこで止めています。
ほかに知っている穴です。三つとも今日、解析の結果を見ました。
- 二重否定。「事故ではないとは言えない」は、「ではない」と「とは言えない」の二つとも反転として取ります。主張に当てれば ambiguous です。意味はほぼ「事故かもしれない」ですが、確定しない側に倒れるので、間違った進行は起きません。
- ひらがなの名詞の前の否定。「事故ではないことは確かだ」は、後ろが「こ」なので文の否定として取ります。名詞の修飾なのに、確定しない側に倒れます。
- 念押しと本気の問いの区別。「佐伯がやったのではないか」は、反転にも質問にも数えず、「がやった」が主張の手掛かりなので、発話の種類は assertion(言い分)になります。
採らなかった案です。
- 否定のスコープを、係り受けの解析で決める。形態素解析と係り受けの解析器が要り、UE・CLI・Linux のサーバの三か所に同じ版を積む必要があります(T26 の回)。規則の取り出しは、どこでも同じ結果になります。
- 否定を点数の減点にする。照合の注記のとおり、言い方を変えると戻ってくるので採りません。
- 否定を見つけたら逆の主張として成立させる。プレイヤーが言っていない結論を、主人公が代わりに言うことになります。
否定は、点数で解くより、原文の位置で解く方が説明しやすい。
確定させずに聞き返す、という逃げ道があるのが、ゲームの強みです。
検索語#
negation scope resolution
*SEM 2012 shared task negation
NegEx negation detection
negation cue scope focus
駐在に「これは事故ではない」と入力してみてください。『霧走町 流氷に眠る』の第一章はブラウザで、プロフィールのリンクから遊べます。