誤解析修復プラグイン
Sudachi の開発初期から検討されている機能のひとつにプラグインによる誤解析修復 (誤解析回復1) があります。
以下のように意図しない解析になってしまったときにルールで強制的に結果を書き換えます。
にはにはにわにわとりがいる。
にわ 名詞,普通名詞,一般,*,*,* 庭
に 助詞,格助詞,*,*,*,* に
はにわ 名詞,普通名詞,一般,*,*,* 埴輪
にわとり 名詞,普通名詞,一般,*,*,* 鶏
が 助詞,格助詞,*,*,*,* が
いる 動詞,非自立可能,*,*,上一段-ア行,終止形-一般 居る
。 補助記号,句点,*,*,*,* 。
EOS
「はにわ」となっている部分をパターンマッチで「は/に/わ」と書き換えるイメージです。
(はにわ,*)/にわとり,* -> は,助詞,係助詞,*,*,*,*/に,名詞,数詞,*,*,*,*/わ,接尾辞,名詞的,助数詞,*,*,*
括弧内が書き換えたい箇所で、表記、品詞などは適宜ワイルドカードにできることを想定しています。
2時間ででっちあげる
実装の面倒さから長い間放置していたのですが、Java の regex をつかって効率度外視でさくっと実装してみることにします。
まず解析結果を以下のような文字列にエンコードします。
0:にわ,名詞,普通名詞,一般,*,*,*/1:に,助詞,格助詞,*,*,*,*/2:はにわ,名詞,普通名詞,一般,*,*,*/3:にわとり,名詞,普通名詞,一般,*,*,*/4:が,助詞,格助詞,*,*,*,*/5:いる,動詞,非自立可能,*,*,上一段-ア行,終止形-一般/6:。,補助記号,句点,*,*,*,*
形態素は'/'区切り、表記と品詞をカンマでつないで先頭にいくつ目の形態素なのかを入れておきます。これを正規表現でマッチさせます。
(\d+:はにわ,.+?)/\d:にわとり
前方参照部分を取り出して対応する形態素を書き換えればできあがりです。
実装
解析結果を書き換えるには PathRewritePlugin
を継承して実装します。プラグインの起動時に setUp()
が呼ばれるので、ここで設定の取得と必要な初期化をしておきます。
@Override
public void setUp(Grammar grammar) {
this.grammar = grammar;
pattern = Pattern.compile(settings.getString("pattern"));
replace = settings.getString("replace");
}
sudachi.json や -s
オプションでわたされた設定は settings
に入っているので getString()
でプロパティ名を指定して取り出します。また品詞名を解析内部でつかわれている品詞IDに変換するために grammar
が必要なのでとっておきます。
最尤パスの推定後、登録された PathRewritePlugin
の rewrite()
が呼び出されます。
@Override
public void rewrite(InputText text, List<LatticeNode> path, Lattice lattice) {
int offset = 0;
Matcher m = pattern.matcher(encodePath(path));
while (m.find()) {
int[] range = getRange(m.group(1));
int targetLength = range[1] - range[0];
List<LatticeNode> target = path.subList(range[0] + offset, range[1] + offset);
int begin = target.get(0).getBegin();
String targetHw = getTargetHeadword(target);
List<WordInfo> replaceWordInfo = getReplaceWordInfo(replace, targetHw);
target.clear();
for (WordInfo wi : replaceWordInfo) {
LatticeNode node = lattice.createNode();
node.setRange(begin, begin + wi.getLength());
node.setWordInfo(wi);
node.setOOV();
target.add(node);
begin += wi.getLength();
}
offset += replaceWordInfo.size() - targetLength;
}
}
パスを文字列にエンコードして正規表現にマッチした部分を書き換えていきます。書き換えたあとの形態素の情報は WordInfo
に入れます。さらに位置情報をつけて (node.setRange()
) ラティスのノードとしてパスに追加します。このとき位置は InputText
内の位置なので UTF-8 でかぞえる必要があります (忘れがち)。ノードはとりあえず未知語にしておきます (node.setOOv()
)。
List
を破壊的に書き換えているのでインデックスがずれる分を offset
に入れておきます (忘れがち)。
WordInfo
をつくるところはこんな感じにします。
List<WordInfo> getReplaceWordInfo(String replace, String targetHw) {
List<WordInfo> wis = new ArrayList<>();
for (String r :replace.split("/")) {
String[] cols = r.split(",");
String hw = cols[0].equals("*") ? targetHw : cols[0];
short length = (short)hw.getBytes(StandardCharsets.UTF_8).length;
short posId = grammar.getPartOfSpeechId(Arrays.asList(cols).subList(1, cols.length));
WordInfo wi = new WordInfo(hw, length, posId, hw, hw, "");
wis.add(wi);
}
return wis;
}
表記は指定されたものか、マッチした部分をいれて、長さは InputText
内の単位なので UTF-8 でかぞえます (忘れがち)。grammar.getPartOfSpeechId()
で品詞IDを取得します。手抜きをして正規化表記と辞書形は表記とおなじにしておきます。読みはなしで。(本来は lattice
内の該当するノードを探すべき)
さてこれでプラグインができました。適当に JAR ファイルにしてクラスパスを通せば Sudachi から利用できるようになります。
$ echo にわにはにわにわとりがいる。 | java -cp jdartsclone-1.2.0.jar:javax.json-1.1.jar:sudachi-0.5.3.jar:bogusrewrite-0.1.0.jar com.worksap.nlp.sudachi.SudachiCommandLine -s '{"pathRewritePlugin":[{"class":"com.worksap.nlp.sudachi.BogusRewritePlugin","pattern":"(\\d+:はにわ,.+?)/\\d:にわとり","replace":"は,助詞,係助詞,*,*,*,*/に,名詞,数詞,*,*,*,*/わ,接尾辞,名詞的,助数詞,*,*,*"}]}'
にわ 名詞,普通名詞,一般,*,*,* 庭
に 助詞,格助詞,*,*,*,* に
は 助詞,係助詞,*,*,*,* は
に 名詞,数詞,*,*,*,* に
わ 接尾辞,名詞的,助数詞,*,*,* わ
にわとり 名詞,普通名詞,一般,*,*,* 鶏
が 助詞,格助詞,*,*,*,* が
いる 動詞,非自立可能,*,*,上一段-ア行,終止形-一般 居る
。 補助記号,句点,*,*,*,* 。
EOS
人名補正プラグイン
Sudachi 開発当初から後日公開予定といわれつづけていまだにリリースされていない機能に人名補正プラグインがあります。人名-姓と敬称にはさまれた形態素はまとめて人名-名に書き換える機能もつ予定です。
この誤解析補正プラグインがあればこの機能も実現できます。
$ echo 山田すだち太郎さん | java -jar sudachi-0.5.3.jar
山田 名詞,固有名詞,人名,姓,*,* 山田
すだち 名詞,普通名詞,一般,*,*,* 酢橘
太郎 名詞,固有名詞,人名,名,*,* 太郎
さん 接尾辞,名詞的,一般,*,*,* さん
EOS
$ echo 山田すだち太郎さん | java -cp jdartsclone-1.2.0.jar:javax.json-1.1.jar:sudachi-0.5.3.jar:bogusrewrite-0.1.0.jar com.worksap.nlp.sudachi.SudachiCommandLine -s '{"pathRewritePlugin":[{"class":"com.worksap.nlp.sudachi.BogusRewritePlugin","pattern":"名詞,固有名詞,人名,姓,\\*,\\*/(.+)/\\d:さん,接尾辞,名詞的,一般,\\*,\\*,\\*","replace":"*,名詞,固有名詞,人名,名,*,*"}]}'
山田 名詞,固有名詞,人名,姓,*,* 山田
すだち太郎 名詞,固有名詞,人名,名,*,* すだち太郎
さん 接尾辞,名詞的,一般,*,*,* さん
EOS
もうこれでいいような気もしますが、正規表現を書くのが大変だし、ルールがひとつしかかけないし、何より実行効率がわるいので、このプラグインはこのままお蔵入りです。
ではよい Sudachi life を。
package com.worksap.nlp.sudachi;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.worksap.nlp.sudachi.dictionary.Grammar;
import com.worksap.nlp.sudachi.dictionary.WordInfo;
public class BogusRewritePlugin extends PathRewritePlugin {
private Grammar grammar;
private Pattern pattern;
private String replace;
@Override
public void setUp(Grammar grammar) {
this.grammar = grammar;
pattern = Pattern.compile(settings.getString("pattern"));
replace = settings.getString("replace");
}
@Override
public void rewrite(InputText text, List<LatticeNode> path, Lattice lattice) {
int offset = 0;
Matcher m = pattern.matcher(encodePath(path));
while (m.find()) {
int[] range = getRange(m.group(1));
int targetLength = range[1] - range[0];
List<LatticeNode> target = path.subList(range[0] + offset, range[1] + offset);
int begin = target.get(0).getBegin();
String targetHw = getTargetHeadword(target);
List<WordInfo> replaceWordInfo = getReplaceWordInfo(replace, targetHw);
target.clear();
for (WordInfo wi : replaceWordInfo) {
LatticeNode node = lattice.createNode();
node.setRange(begin, begin + wi.getLength());
node.setWordInfo(wi);
node.setOOV();
target.add(node);
begin += wi.getLength();
}
offset += replaceWordInfo.size() - targetLength;
}
}
String encodePath(List<LatticeNode> path) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.size(); i++) {
if (i != 0) {
sb.append("/");
}
sb.append(i).append(":");
encodeNode(sb, path.get(i));
}
return sb.toString();
}
StringBuilder encodeNode(StringBuilder sb, LatticeNode node) {
WordInfo wi = node.getWordInfo();
String surface = wi.getSurface();
String pos = String.join(",", grammar.getPartOfSpeechString(wi.getPOSId()));
sb.append(surface).append(",").append(pos);
return sb;
}
int[] getRange(String encodedNodes) {
int[] range = new int[2];
String[] nodes = encodedNodes.split("/");
range[0] = Integer.parseInt(nodes[0].split(":")[0]);
range[1] = Integer.parseInt(nodes[nodes.length - 1].split(":")[0]) + 1;
return range;
}
String getTargetHeadword(List<LatticeNode> path) {
return path.stream().map(n -> n.getWordInfo().getSurface()).collect(Collectors.joining());
}
List<WordInfo> getReplaceWordInfo(String replace, String targetHw) {
List<WordInfo> wis = new ArrayList<>();
for (String r :replace.split("/")) {
String[] cols = r.split(",");
String hw = cols[0].equals("*") ? targetHw : cols[0];
short length = (short)hw.getBytes(StandardCharsets.UTF_8).length;
short posId = grammar.getPartOfSpeechId(Arrays.asList(cols).subList(1, cols.length));
WordInfo wi = new WordInfo(hw, length, posId, hw, hw, "");
wis.add(wi);
}
return wis;
}
}
-
たぶんATOK用語。 ↩