「十五日」とだけ入力すると、船長は「何についてですか」と返す
Type only "the 15th" and the captain asks "about what?"
D06-3-T11-temporal-expressions・技術・日本語入力でプレイする仕組み
港の船長に「2月15日の夜はどこにいましたか」と入力すれば、事件の夜の居場所の証言が返ります。
「十五日」とだけ入力すると、「十五日の、何についてですか」と聞き返されます。
「2月30日」と入力しても、同じように聞き返されます。
この回の位置と、前後の回#
区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 9 回目)
先に読むとよい回: T10(固有名詞の辞書)
次に読む回: T12(複文を節に割る)
名前 — temporal expression recognition and normalization#
- 正式名
- temporal expression recognition and normalization(時間表現の抽出と正規化)
- 略称
- TERN(ACE の評価の名前でもある)
- 日本語
- 時間表現抽出、時間表現の正規化
- 注釈の規格
- TimeML と、その中の時間表現の札 TIMEX3
- 初出
- TimeML は Pustejovsky, Castaño, Ingria, Saurí, Gaizauskas, Setzer, Katz, Radev, "TimeML: Robust Specification of Event and Temporal Expressions in Text"(2003、AAAI Spring Symposium)。TIMEX3 は、それより前の TIDES の TIMEX2 を拡げた札です。
時間表現の抽出と正規化とは#
二段の仕事です。
一段目の抽出は、文の中から時間を表す区間を見つけることです。
「2月15日」「午後十時」「三日間」「毎週月曜」「昨日」。
二段目の正規化は、その区間を機械が比べられる値にすることです。
TimeML の TIMEX3 では、区間に type と value を付けます。
type は DATE・TIME・DURATION・SET の 4 種類。
value は ISO 8601 に寄せた書き方で、年が分からない 2 月 15 日なら XXXX-02-15、期間の三日間なら P3D のように書きます。
正規化でいちばん難しいのは、相対的な表現です。
「昨日」「先週の金曜」は、それだけでは日付になりません。
文書の日付(document creation time)か、文脈の中の別の時刻に錨を下ろして、はじめて値が決まります。
TimeML が「錨を下ろす」「並べる」「あいまいなまま書く」の三つを扱う、と論文の要旨で言っているのはこのためです。
作り方は、長いあいだ規則が強い分野でした。
TempEval-2(2010)のために作られた HeidelTime は、正規表現と正規化の規則の束で TIMEX3 を出し、TempEval-2・TempEval-3 の英語で上位を取っています。
数字の書き方・単位・前後の語が言語ごとに決まっているので、規則で書きやすいのです。
よく知られた弱点は三つです。
- 錨。相対表現の値は、基準の時刻を取り違えると全部ずれる。
- 粒度。「夜」「夕方」は幅のある区間で、何時から何時か決まらない。
- 数字の曖昧さ。日本語なら「一日」は「ついたち」か「いちにち」か、「十日」は日付か期間か。
うちで取り出すもの#
入力の解析(AdvInput.hpp)が持つ時間の型はこれだけです。
struct DateMention
{
int month = 0, day = 0;
bool hasMonth = false;
bool valid = true; // 2月30日 のような存在しない日付は false
InputSpan source;
};
struct TimeMention
{
int hour = 0, minute = 0;
bool hasMinute = false;
bool valid = true; // 25時 などは false
InputSpan source;
};
ほかに「年を含む表現があったか」(yearMentioned)と、「日時と助詞だけでできた入力か」(dateFragment)を持ちます。
source は原文の UTF-8 の byte 区間です。
聞き返しの台詞に差し込む「十五日」は、この区間で原文から切り出します。
正規化した後の文字列から作り直さないので、入力した表記がそのまま返ります。
年・期間・相対表現(昨日・先週)は取り出しません。
作品の暦は adv.json の inputPolicy に書きます。
"calendar": "gregorian-month-day"
月日だけを扱う、という宣言です。
"none" の作品では、日時の確認も日付の規則も動きません。
数字の読み方#
数字は ASCII・全角・漢数字を読みます。ReadNumber の漢数字の部分を写します。
// 漢数字: 値10 の文字は桁の乗数(二十一 = 2*10+1・十五 = 15)
int Total = 0, Cur = -1;
while (i < S.size())
{
const auto F = L.numerals.find(S[i].c);
if (F == L.numerals.end()) { break; }
if (F->second == 10) { Total += (Cur < 0 ? 1 : Cur) * 10; Cur = -1; }
else
{
if (Cur >= 0) { Cur = Cur * 10 + F->second; } // 「二〇」のような位取り記法
else { Cur = F->second; }
}
++i;
}
「十五」は 15、「二十」は 20、「二〇」は位取りで 20 です。
百・千は読みません。
日時に要る数は 31 と 59 までなので、足りています。
漢数字の表(〇零一〜十)も単位(月・日・時・分・半・年)も、C++ には置かず、共通の言語資源 input.json から読みます。
日付と時刻の規則#
解析は左から 1 文字ずつ進み、数字を読めた所で、すぐ後ろの単位を見ます。
- 数字+「:」+ちょうど 2 桁 … 時刻(22:30)。全角の「22:30」も畳んで同じに読みます。
- 数字+「年」 … yearMentioned を立てて読み飛ばす。
- 数字+「月」+数字+「日」 … 月日。
- 数字+「日」 … 月の無い日付。
- (午前・午後)+数字+「時」(+数字+「分」か「半」) … 時刻。
単位の後ろに続くと日時にしない語も、言語資源にあります。
"dayNotAfter": ["間", "前", "後", "目", "分", "中", "おき", "ぶり"],
"hourNotAfter": ["間", "代", "点", "期", "刻"],
「三日間」「十日前」は日付になりません。
「二時間」「十時代」は時刻になりません。
「一日目」も日付になりません。
存在の判定はこう書いてあります。
D.valid = N1 >= 1 && N1 <= 12 && N2 >= 1 && N2 <= DaysInMonth(N1);
2 月は 29 日まで通します。
年を持たないので、うるう年かどうかが分からないからです。
時刻は 0〜24 時(24 時は 0 分だけ)、分は 0〜59。
午後は 12 未満に 12 を足し、午前 12 時は 0 時にします。
午前・午後を付けて 12 より大きい数を書いたら(午後十三時)、無効です。
分かれ先は三つ#
取り出した日時は、三か所で使います。
一つ目、日時の確認。
この条件のどれかなら、照合をせずに「何についてか」を尋ねます。AdvSession.hpp から写します。
bool needsDateAsk(const InputAnalysis& A) const
{
if (A.yearMentioned) { return false; }
for (const DateMention& D : A.dates) { if (!D.valid) { return true; } }
for (const TimeMention& T : A.times) { if (!T.valid) { return true; } }
// 裸の日付(対象の分からない日時だけの入力)
return A.dateFragment && !A.dates.empty() && A.times.empty() && !A.fragmentHasSuffix &&
A.speech == SpeechKind::Unknown;
}
存在しない日時と、裸の日付は、補わずに聞き返す。
「十五日」から「事件の夜のことだろう」と推して答えると、プレイヤーが言っていないことを主人公が代わりに言うことになるからです。
聞き返された後に「あなたの居場所について」と入力すると、前の入力と改行でつないで解析し直します。
今日は 0.590 で居場所の証言に届きました。
二つ目、応答契約の日付。
作品の dialogue.json に、日付を条件にした規則を書けます。
第一章の佐伯の規則は 2 本です。
{ "id": "saeki-event-night-query", "speaker": "saeki", "phase": [1], "intent": "saeki.alibi_night",
"all": [["どこ", "居場所", "何をして", "何して", "なにして", "について", "教えて", "おしえて", "聞かせて", "過ごし", "いた", "いました"]],
"match": { "date": { "month": 2, "day": 15, "allowDayOnly": true }, "speech": "question" },
"excludes": [ …別の話題の語 9 個… ], "bindEntities": ["alibi"] }
{ "id": "saeki-event-night-fragment", "speaker": "saeki", "phase": [1], "intent": "saeki.alibi_night",
"match": { "date": { "month": 2, "day": 15, "allowDayOnly": true }, "speech": "fragment" }, "bindEntities": ["alibi"] }
一致の判定(DialogueDateMatches)は厳しめです。
年があれば外れ、無効な日時が一つでもあれば外れ、別の日付が一つでもあれば外れます。
allowDayOnly なら月の無い「十五日」でも通ります。
speech の fragment は「日時と助詞だけの入力で、時刻か『について』などの語尾がある」形です。
「2月15日の二十時から22時」は、日時を取り除いた残り「のから」を助詞の一覧(の・から)で食べ切れるので省略形になり、fragment の規則で居場所の証言に着地しました。
bindEntities は、この規則が通ったときに「材料として alibi を挙げた」と見なしてよい、という印です。
補ってよい組は adv.json の inputPolicy.bindableEntities に書いたものだけで、読み込みのときに検査します。
三つ目、意図の日付の見張り。
意図に inputGuard.date を書くと、問いの日付で候補から外せます。
inline bool InputGuardAllows(const Intent& It, const ClaimRegistry& Claims, const InputAnalysis& Analysis)
{
const IntentInputGuard& G = It.inputGuard;
if (!G.present) { return true; }
if (G.day)
{
bool bAnyDate = false, bMatch = false;
for (const DateMention& D : Analysis.dates)
{
if (Analysis.inCitation(D.source)) { continue; }
bAnyDate = true;
if (D.valid && D.day == G.day && (!D.hasMonth || D.month == G.month)) { bMatch = true; }
}
if (bAnyDate && !bMatch) { return false; }
}
// …(この後の除外語・除外の主張の判定は省略)
}
日付を一つも書いていない問いは通します。
書いた日付に宣言の日が一つでもあれば通します(「十五日と十六日の夜」)。
宣言の日が無く別の日だけなら外します。
引用の中の日付(「〜日だ」と言った)は数えません。
佐伯の居場所の意図は 2 月 15 日を宣言しているので、今日「2月16日の夜はどこにいましたか」と入力すると、その意図は外れ、別の人物への誘導になりました。
「十五日と十六日の夜はどこにいましたか」は 0.711 で採用です。
名前どおりでない所 — temporal expression recognition and normalization#
TimeML/TIMEX3 の、ごく狭い部分集合です。
- type は DATE と TIME だけ。DURATION(三日間)と SET(毎晩)は抽出もせず、むしろ日付にしないための除外語として使っています。
- value は月・日・時・分の整数だけで、年は持ちません。TIMEX3 で言えば XXXX-02-15 と T22:00 の範囲です。
- 錨を下ろしません。「昨日」「あの晩」は時間表現として正規化せず、固有名詞の辞書(T10 の回)の alibi の別名として扱います。ゲームの中の「今日」がいつかを、入力の解析は知りません。
- 事件(EVENT)や時間の前後関係(TLINK)は扱いません。
抽出から使い道までが一本の規則の束で、HeidelTime と同じく規則で書いた側です。
時間帯の語は取り出していない#
計画の段階では「日付・時刻・時間帯」と書いていましたが、ソースを読むと、入力の解析は「夜」「晩」「朝」を取り出しません。
データの注記で「時間帯」と呼んでいるのは「二十時から22時」のような時刻の幅のことでした。
意図の timeOfDay(昼・夜)は、いまの場面が昼か夜かで候補を絞るゲームの状態で、入力の解析とは別物です。
その結果、今日いちばん困ったのがこの入力です。
「2月15日の夜について」→ 受け流し(最高 0.229)
日付は取れていますが、残りの「の夜について」は助詞の一覧で食べ切れないので省略形になりません。
疑問の手掛かりも無いので、question の規則にも当たりません。
照合では、短い入力の埋め込みが弱く、聞き返しの下限 0.27 にも届きませんでした。
「2月15日の夜はどこにいましたか」なら通るので、言い直せば進みます。
それでも、刑事が手帳を見ながら入力しそうな形で落ちるのは弱点です。
弱点と、採らなかった案 — temporal expression recognition and normalization#
- 「夜」「晩」「夕方」を時間帯として取り出し、省略形の残りとして食べさせる案。いちばん素直な直し方です。ただ「夜」を食べてよいかは作品の暦の問題で、言語資源に足すと、夜の話が無い作品でも省略形の判定が広がります。作品の inputPolicy の fragmentSuffixes に「の夜について」を足す方が、影響が作品の中に閉じます。
- 相対表現(昨日・一昨日)を、ゲームの中の日付に錨を下ろして正規化する案。ゲームの状態にいまの日付を持たせる必要があり、セーブの形も変わります。いまは「昨日の夜」を辞書で受ける方が、取りこぼしが少ないと判断しています。
- 期間(三日間)や回数(毎晩)を取り出す案。使い道が無いので入れていません。除外語としての働きだけで足りています。
- 日付を推して補う案(十五日 → 事件の夜)。採りません。プレイヤーが言っていないことを主人公に言わせない、が入力の解析の約束だからです。
時間の表現は、規則で書くと規則の外がはっきり見えます。
「2月15日の夜について」が落ちるのも、ログを見れば理由が一行で分かります。
検索語#
temporal expression recognition and normalization
TimeML TIMEX3
HeidelTime rule-based temporal tagger
TempEval-2
事件の夜の日付を、いろいろな書き方で船長に入力してみてください。『霧走町 流氷に眠る』の第一章はブラウザで、プロフィールのリンクから遊べます。