文区切りをいい感じに実行したい
こんなことを書くよ
- 文区切りをいい感じにやっといてくれるライブラリの紹介
- ちょっと不都合があったので、改造して使う場合の紹介
文区切り?なんで必要?
「文」を扱いたい自然言語処理タスクのときに、文区切りが必要になる。
例えば、固有表現表現抽出をするときは、文区切りをしてから実行する(イメージ。ぼくの頭の中で)
ここ2年くらいだと、Bertを使うタスクのときは、割と文区切りをすることが多いと思う。主にメモリ制約の問題などで。
文区切り?句点で切ればいいんじゃないの?
ところがどっこい、そうはいかない。
新聞記事以外の日本語では、句点以外の文区切りパターンが存在している。
- クエスチョンマークとエクスクラメーションマーク
- 顔文字
さらに、こういうパターンに起き得る。
- 直接引用文の中身に句点、?、!が書かれている。でも、本文は終了していないので、区切ってほしくない
- Ex. ぼくはいいんじゃいかな?と思う
- 見た目の整形のために、文途中で改行を入れる人がいる。特にメールでは多いと思う。
- ぼくも「しゃかいじんまなー」とかいうやつでそう指導された。でも、しょーじき言ってどーでもいいレベルといまは思ってる。でも、くせになってしまったので、結局はぼくも改行してしまっている。
- 時々、英語・ドイツ語のメールを書いているときも「文途中で改行いれて整形したい病」に襲われる。悪しき弊害だな〜。
と、いう、あれやこれやを自分でルールを書いていくと相当に面倒くさい。
どうすんのよ?
ja-sentence-segmenterっていう割といい感じにやってくれるパッケージがある。
ちなみに、Qiitaにも作者の書いた記事がある。
こいつのすごい点は次の2点だ。
- 直接引用文の中身で文区切りしないように工夫してくれている。
- 文区切り中の処理(全角・半角変換、文字区切り)の処理を自分で任意の処理で置き換えることができる。
詳細な使い方は、Qiitaの記事を見てもらえれば〜。
ちょっと困った
いろいろ事情があって「改行を消さないで残してほしい」っていうユースケースが発生した。
ちょっと苦戦したので、メモとして残しておく。ちな、ja-sentence-segmenterのバージョンは0.02
何に困った?
1 2 3 4 5 6 7 |
こういう文が 入力文だとしよう。 改行が途中ではいっているけど、 改行を消さないでほしい。 句点で文を区切ってほしいけど、 改行は\nとして残してほしい。 でも、ja-sentence-segmenterのサンプルコードだと改行が消えてしまう仕様。 |
サンプルコードをそのまま実行すると、こうなる。
1 |
['こういう文が', '入力文だとしよう。', '改行が途中ではいっているけど、', '改行を消さないでほしい。', '句点で文を区切ってほしいけど、', '改行は', 'として残してほしい。', 'でも、ja-sentence-segmenterのサンプルコードだと改行が消えてしまう仕様。'] |
改行が消えてしまう原因は2箇所に問題があった。
- サンプルコードのsplit_newline関数
- サンプルコードのsplit_punctuation関数
この2つの関数と自分で作成して、パイプラインに入れてやれば良い。
で、こうした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
from typing import Iterator, Generator, Match import re BETWEEN_QUOTE_JA_REGEX = r"「[^「」]*」" BETWEEN_PARENS_JA_REGEX = r"\([^()]*\)" ESCAPE_CHAR = "∯" DEFAULT_PUNCTUATION_REGEX = r"。!?" def __split_newline_iter(texts: Iterator[str]) -> Generator[str, None, None]: for text in texts: # この行を変更。keepends=Trueに変更する。 for line in text.splitlines(keepends=True): yield line def custom_split_newline(arg: Union[str, List[str], Iterator[str]]) -> Generator[str, None, None]: """Split text with line boundaries. Parameters ---------- arg : Union[str, List[str], Iterator[str]] texts you want to split. Yields ------ Generator[str, None, None] texts splitted with line boundaries. """ if isinstance(arg, str): yield from __split_newline_iter(iter([arg])) elif isinstance(arg, list): yield from __split_newline_iter(iter(arg)) elif isinstance(arg, Iterator): yield from __split_newline_iter(arg) def __split_punctuation_iter(texts: Iterator[str], punctuations: str, split_between_quote: bool, split_between_parens: bool) -> Generator[str, None, None]: def escape_between_punctuation(match: Match[str]) -> str: text = match.group() escapeRegex = rf"(?<!{ESCAPE_CHAR})([{punctuations}])(?!{ESCAPE_CHAR})" result = re.sub(escapeRegex, rf"{ESCAPE_CHAR}\1{ESCAPE_CHAR}", text) return result def escape_between_quote(text: str) -> str: result = re.sub(BETWEEN_QUOTE_JA_REGEX, escape_between_punctuation, text) return result def escape_between_parens(text: str) -> str: result = re.sub(BETWEEN_PARENS_JA_REGEX, escape_between_punctuation, text) return result def sub_split_punctuation(text: str) -> List[str]: splitRegex = rf"(?<!{ESCAPE_CHAR})([{punctuations}])(?!{ESCAPE_CHAR})" # 文区切りマーカーを\nから[EOS]に変更 result = re.sub(splitRegex, "\\1[EOS]", text) unescapeRegex = rf"({ESCAPE_CHAR})([{punctuations}])({ESCAPE_CHAR})" result = re.sub(unescapeRegex, "\\2", result) # 区切り処理を実行 return re.split(r'\[EOS\]', result) for text in texts: temp = text if not split_between_quote: temp = escape_between_quote(temp) if not split_between_parens: temp = escape_between_parens(temp) sentences = sub_split_punctuation(temp) for sentence in sentences: yield sentence def custom_split_punctuation( arg: Union[str, List[str], Iterator[str]], punctuations: str = DEFAULT_PUNCTUATION_REGEX, split_between_quote: bool = False, split_between_parens: bool = False, ) -> Generator[str, None, None]: """Split text with puctuations. Parameters ---------- arg : Union[str, List[str], Iterator[str]] texts you want to split punctuations : str, optional regular expression for puctuations, by default DEFAULT_PUNCTUATION_REGEX split_between_quote : bool, optional split if punctuation between quotes, by default False split_between_parens : bool, optional split if punctuation between parentheses, by default False Yields ------ Generator[str, None, None] texts splitted with puctuations. """ if isinstance(arg, str): yield from __split_punctuation_iter(iter([arg]), punctuations, split_between_quote, split_between_parens) elif isinstance(arg, list): yield from __split_punctuation_iter(iter(arg), punctuations, split_between_quote, split_between_parens) elif isinstance(arg, Iterator): yield from __split_punctuation_iter(arg, punctuations, split_between_quote, split_between_parens) |
で、最後にパイプラインを作ってやる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import functools from ja_sentence_segmenter.common.pipeline import make_pipeline from ja_sentence_segmenter.concatenate.simple_concatenator import concatenate_matching from ja_sentence_segmenter.normalize.neologd_normalizer import normalize from ja_sentence_segmenter.split.simple_splitter import split_newline, split_punctuation # ここを変更した split_punc2 = functools.partial(custom_split_punctuation, punctuations=r"。!?") concat_tail_te = functools.partial(concatenate_matching, former_matching_rule=r"^(?P<result>.+)(て)$", remove_former_matched=False) # ここを変更した segmenter = make_pipeline(normalize, custom_split_newline, concat_tail_te, split_punc2) print(list(segmenter(text))) |
実行してみる。
1 |
['こういう文が\n', '入力文だとしよう。', '\n', '改行が途中ではいっているけど、\n', '改行を消さないでほしい。', '\n', '句点で文を区切ってほしいけど、\n', '改行は\n', 'として残してほしい。', '\n', 'でも、ja-sentence-segmenterのサンプルコードだと改行が消えてしまう仕様。', ''] |
うまく行った!
作者の方が柔軟も変更にできる設計にしてくれているので、モンキーパッチみたいなことをしなくても良かった。すばらしい?
ディスカッション
コメント一覧
まだ、コメントがありません