ボーネンLT会 はんなりプログラミングの会
2022/12/16 nikkie
日本語は文の中の単語が区切られていない
形態素解析 して
単語分割=分かち書き
品詞付与
言語, 辞書,コーパスに依存しない汎用的な設計
MeCabの辞書の1つ mecab-ipadic-NEologd
解析前に行うことが望ましい文字列の正規化処理
https://github.com/neologd/mecab-ipadic-neologd/wiki/Regexp.ja
これを 理解し、真似る のが今回のLTです
スペース削除
置換
全角・半角変換
>>> " テキストの前".strip()
'テキストの前'
>>> "テキストの後 ".strip()
'テキストの後'
https://docs.python.org/ja/3/library/stdtypes.html#str.strip
re.sub(pattern, repl, string)
string 中に出現する最も左の重複しない pattern を置換 repl で置換することで得られる文字列を返します。
https://docs.python.org/ja/3/library/re.html#re.sub
❗️replが関数のときは、動きがちょっと異なります
ハイフンマイナスっぽい文字は -
HYPHEN-MINUS に
長音記号っぽい文字は ー
KATAKANA-HIRAGANA PROLONGED SOUND MARK に
1回以上連続する長音記号は1つにまとめる
>>> import re
>>> # ハイフンマイナスっぽい文字を揃える
>>> re.sub("[˗֊‐‑‒–⁃⁻₋−]+", "-", "o₋o")
'o-o'
>>> # 長音記号っぽい文字を揃える
>>> # & 1回以上連続する長音記号は1つにまとめる(replのーはpatternに含まれている)
>>> re.sub("[﹣-ー—―─━ー]+", "ー", "majika━")
'majikaー'
>>> re.sub("[﹣-ー—―─━ー]+", "ー", "スーパーーーー")
'スーパー'
チルダっぽい文字は削除
>>> re.sub("[~∼∾〜〰~]", "", "わ〰い")
'わい'
unicodedata.normalize(form, unistr)
Unicode 文字列 unistr の正規形 form を返します。form の有効な値は、'NFC'、'NFKC'、'NFD'、'NFKD' です。
https://docs.python.org/ja/3/library/unicodedata.html#unicodedata.normalize
>>> import unicodedata
>>> unicodedata.normalize("NFKC", "012ABCxyz")
'012ABCxyz'
>>> hankaku = "ハンガク"
>>> len(hankaku)
5
>>> unicodedata.normalize("NFKC", hankaku)
'ハンガク'
>>> len(_)
4
>>> import re
>>> import unicodedata
>>> # 全角英数字と半角カタカナを表す正規表現
>>> pattern = re.compile("([0-9A-Za-z。-゚]+)")
>>> "".join(
... unicodedata.normalize("NFKC", x) if pattern.match(x) else x
... for x in re.split(pattern, "012ABCxyzはハンガク")
... )
'012ABCxyzはハンガク'
すべての文字をNFKCに正規化するアプローチもあります(『BERTによる自然言語処理入門』)
「NFKCとかNFKDとかってなんだ!?」👉『プログラマのための文字コード技術入門』付録A.4
半角に揃えたい記号
全角に揃えたい記号
以下の全角記号は半角に置換
/!”#$%&’()*+,−./:;<>?@[¥]^_`{|}
以下の半角記号は全角に置換
。、・=「」
記号を 全角に揃える
半角に置換する記号だけ置換する
str.translate(table)
与えられた変換テーブルに基づいて文字列を構成する各文字をマッピングし、マッピング後の文字列のコピーを返します。
https://docs.python.org/ja/3/library/stdtypes.html#str.translate
str.translate(table)
文字から文字への異なる形式のマッピングから変換マップを作成するために、
str.maketrans()
が使えます。
https://docs.python.org/ja/3/library/stdtypes.html#str.translate
str.maketrans
>>> mapping = str.maketrans(
... '!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」',
... "!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」"
... )
>>> ord("#")
35
>>> mapping[ord("#")] # Unicodeコードポイント間の変換を表す
65283
>>> chr(_)
'#'
str.translate
>>> "!#".translate(mapping)
'!#'
記号を全角に揃えられる!
再度の unicodedata.normalize
>>> unicodedata.normalize("NFKC", "!#") # 半角に戻る
'!#'
>>> pattern = re.compile("([!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜]+)")
>>> "".join(
... unicodedata.normalize("NFKC", x) if pattern.match(x) else x
... for x in re.split(pattern, "!。#".translate(mapping))
... )
'!。#'
>>> # 全角にしたクォートは半角に戻らない
>>> unicodedata.normalize("NFKC", "”%’")
'”%’'
>>> # re.subで半角に戻す
>>> re.sub('[’]', "'", _) # RIGHT SINGLE QUOTATION MARK
"”%'"
>>> re.sub('[”]', '"', _) # RIGHT DOUBLE QUOTATION MARK
'"%\''
先頭と末尾の半角スペース以外も
全角スペースは半角スペースに置換
1つ以上の半角スペースは、1つの半角スペースに置換
s = re.sub('[ ]+', ' ', s)
「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」間に含まれる場合
消える例:「アイ の 歌声 を 聴か せ て」
「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間に含まれる場合
消える例:「アルゴリズム C」
>>> basic_latin = "\u0000-\u007F" # 半角英数字
>>> # 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」
>>> blocks = "".join(
... (
... "\u4E00-\u9FFF", # CJK UNIFIED IDEOGRAPHS
... "\u3040-\u309F", # HIRAGANA
... "\u30A0-\u30FF", # KATAKANA
... "\u3000-\u303F", # CJK SYMBOLS AND PUNCTUATION
... "\uFF00-\uFFEF", # HALFWIDTH AND FULLWIDTH FORMS
... )
... )
>>> pattern = re.compile("([{}]) ([{}])".format(blocks, basic_latin))
>>> m = pattern.search("アルゴリズム C")
>>> m.group(1)
'ム'
>>> m.group(2)
'C'
>>> # \1 は \g<1> と等価。グループ番号1がマッチした部分文字列
>>> pattern.sub(r"\1\2", "アルゴリズム C") # 半角スペースを削除
'アルゴリズムC'
>>> pattern = re.compile("([{}]) ([{}])".format(blocks, blocks))
>>> s = "アイ の 歌声 を 聴か せ て"
>>> while pattern.search(s):
... s = pattern.sub(r"\1\2", s)
>>> s
'アイの歌声を聴かせて'
分かち書きが戻せた!
部品化 した処理を 組合せる 実装を提案
「解析前に行うことが望ましい文字列の正規化処理」が何をしているかは分かった
自分だったらどう実装するか を考えてみる
どんなAPIにしようか、OSSを観察
正規化処理をする Normalizers
Pythonで使える。ただし、Rust実装。pyiファイル を 観察
ベースクラス Normalizer
normalize_str(text)
は text を正規化した文字列を返す
NFKC
:Unicode正規化処理だけするNormalizer
Strip
や Replace
など
Normalizer
クラスを継承しているので、どれも normalize_str
を呼び出せばよい
Sequence
がにくいいくつかの正規化処理をまとめた正規化処理 を表す
Normalizer
を継承しているので normalize_str
を呼び出せばよい
typing.Protocol
https://docs.python.org/ja/3/library/typing.html#typing.Protocol
ダックタイピングを表せる型ヒント(という理解)
抽象基底クラス abc.ABC
を使っても全然できると思います
Normalizer
プロトコル@runtime_checkable
class Normalizer(Protocol):
def normalize(self, text: str) -> str:
...
normalize
メソッドがあれば(継承していなくても)Normalizer
>>> class C:
... def normalize(self, text: str) -> str:
... return "spam"
>>> issubclass(C, Normalizer), isinstance(C(), Normalizer)
(True, True)
>>> class D:
... ...
>>> issubclass(D, Normalizer)
False
>>> class E:
... def normalize(self): # シグネチャはプロトコルと一致しない
... return "e"
>>> issubclass(E, Normalizer), isinstance(E(), Normalizer)
(True, True)
class Strip(Normalizer):
def normalize(self, text: str) -> str:
return text.strip()
class Replace(Normalizer):
def __init__(self, pattern, repl) -> None:
self.pattern = pattern
self.repl = repl
def normalize(self, text: str) -> str:
return re.sub(self.pattern, self.repl, text)
Sequence
class Sequence(Normalizer):
"""
>>> sut = Sequence([Lowercase(), NFKC()])
>>> sut.normalize("NikkiE")
'nikkie'
>>> sut.normalize("".join([chr(0X30D5), chr(0X309A), chr(0X30ED)]))
'プロ'
"""
def __init__(self, normalizers: AbcSequence[Normalizer]) -> None:
self.normalizers = normalizers
def normalize(self, text: str) -> str:
normalized = text
for normalizer in self.normalizers:
normalized = normalizer.normalize(normalized)
return normalized
class RemoveExtraSpaces(Normalizer):
BLOCKS = "".join(
(
"\u4E00-\u9FFF", # CJK UNIFIED IDEOGRAPHS
"\u3040-\u309F", # HIRAGANA
"\u30A0-\u30FF", # KATAKANA
"\u3000-\u303F", # CJK SYMBOLS AND PUNCTUATION
"\uFF00-\uFFEF", # HALFWIDTH AND FULLWIDTH FORMS
)
)
BASIC_LATIN = "\u0000-\u007F"
def __init__(self):
self.normalizer = Sequence(
[
Replace("[ ]+", " "),
RemoveSpaceBetween(self.BLOCKS, self.BLOCKS),
RemoveSpaceBetween(self.BLOCKS, self.BASIC_LATIN),
RemoveSpaceBetween(self.BASIC_LATIN, self.BLOCKS),
]
)
def normalize(self, text: str) -> str:
return self.normalizer.normalize(text)
def normalize_neologd(s):
normalizer = NeologdNormalizer()
return normalizer.normalize(s)
if __name__ == "__main__":
assert "0123456789" == normalize_neologd("0123456789")
assert "ABCDEFGHIJKLMNOPQRSTUVWXYZ" == normalize_neologd(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
Norm@lizer
mecab-ipadic-NEologdの文字列の正規化処理を理解し、真似た
正規表現や str.translate
を駆使して正規化している
🤗/tokenizersを参考にし、処理を部品化した実装を提案
さよなら2022年!!!🐯
mecab-ipadic-NEologd、学びをありがとう!