hojichar.filters.document_filters
1import json 2import logging 3import pathlib 4import re 5import string 6import time 7import unicodedata 8from collections import Counter 9from itertools import groupby 10from os import PathLike 11from typing import Any, Dict, List, Optional, Union 12 13import numpy as np 14 15import hojichar 16from hojichar.core.filter_interface import Filter 17from hojichar.core.models import Document, Token 18 19try: 20 import emoji 21 from fugashi import Tagger # type: ignore 22 23 is_loaded_extras = True 24except ImportError: 25 is_loaded_extras = False 26 27BASE_PATH = pathlib.Path(hojichar.__path__[0]) 28logger = logging.getLogger(__name__) 29 30 31class ExampleHojiChar(Filter): 32 """基本的なフィルタの実装例です. 末尾に'<hojichar>'を追加します.""" 33 34 def apply(self, document: Document) -> Document: 35 """ 36 >>> ExampleHojiChar()("hello, world") 37 'hello, world<hojichar>' 38 """ 39 document.text += "<hojichar>" 40 return document 41 42 43class ExampleDiscardDocumentContainKeyword(Filter): 44 """特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です.""" 45 46 def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None: 47 super().__init__(*args, **kwargs) 48 self.keyword = keyword 49 50 def apply(self, document: Document) -> Document: 51 """ 52 >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected 53 True 54 """ 55 if self.keyword in document.text: 56 document.is_rejected = True 57 return document 58 59 60class Identity(Filter): 61 """何も変化を加えないフィルタです. テスト・デバッグに用いられます.""" 62 63 def apply(self, document: Document) -> Document: 64 return document 65 66 67class DiscardAll(Filter): 68 """ 69 すべてのドキュメントを破棄するフィルタです. 70 テスト・デバッグに用いられます. 71 """ 72 73 def apply(self, document: Document) -> Document: 74 document.is_rejected = True 75 return document 76 77 78class ApplyDiscard(Filter): 79 """ 80 上流フィルタで破棄された`Document`を空文字列にします. 81 82 `Document.is_rejected=True` の ドキュメントは無視されるため, 83 このフィルタを `Compose` のコンストラクタに渡しても動作しません. 84 このフィルタは主に`Compose` 内部や, `discard_filtered=False` を指定 85 したデバッグ時などに利用されます. 86 """ 87 88 def __init__(self, *args: Any, **kwargs: Any) -> None: 89 super().__init__(*args, **kwargs) 90 91 def apply(self, document: Document) -> Document: 92 """ 93 >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text 94 '' 95 """ 96 if document.is_rejected: 97 document.text = "" 98 99 return document 100 101 102class Sleep(Filter): 103 """ 104 デバッグ用のフィルタです. 指定秒スリープします. 105 """ 106 107 def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None: 108 super().__init__(*args, **kwargs) 109 self.time = time 110 111 def apply(self, document: Document) -> Document: 112 """ 113 >>> Sleep(0.1)('hello') # After 0.1 seconds, 114 'hello' 115 """ 116 time.sleep(self.time) 117 return document 118 119 120class DocumentNormalizer(Filter): 121 """ 122 Unicode の正規化をします. 123 """ 124 125 def __init__(self, *args: Any, **kwargs: Any) -> None: 126 super().__init__(*args, **kwargs) 127 128 def apply(self, document: Document) -> Document: 129 document.text = unicodedata.normalize("NFKC", document.text) 130 return document 131 132 133class JSONLoader(Filter): 134 """ 135 テキストを Json として解釈し, `key` で指定した要素を文字列として 136 doument に格納します.デフォルトの `key` は 'text' です. 137 138 Json の読み込み, あるいは `key` の読み込みに失敗した際には例外を送出します. 139 これらを無視する場合は, `ignore=True` にします. その際, 読み込みに失敗 140 したドキュメントは破棄されます. 141 """ 142 143 def __init__( 144 self, 145 key: str = "text", 146 ignore: bool = False, 147 extra_keys: Optional[List[str]] = None, 148 *args: Any, 149 **kwargs: Any, 150 ) -> None: 151 super().__init__(*args, **kwargs) 152 self.key = key 153 self.ignore = ignore 154 self.extra_keys = extra_keys 155 156 def apply(self, document: Document) -> Document: 157 """ 158 >>> JSONLoader()( '{"text": "hello, world", "words": 2}' ) 159 'hello, world' 160 161 >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON 162 Traceback (most recent call last): 163 ... 164 json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9) 165 166 >>> JSONLoader()( '{"words": 2}' ) 167 Traceback (most recent call last): 168 ... 169 KeyError: 'text' 170 171 >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected 172 True 173 """ 174 try: 175 data = json.loads(document.text) 176 document.text = str(data[self.key]) 177 if self.extra_keys is not None: 178 document.extras = {key: data[key] for key in self.extra_keys if key in data} 179 except Exception as e: 180 logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}") 181 if self.ignore: 182 document.is_rejected = True 183 return document 184 else: 185 raise e 186 187 return document 188 189 190class JSONDumper(Filter): 191 """ 192 Document.text の文字列を json に変換します. 193 必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。 194 デフォルトで `skip_rejected` が `False` にセットされており、Document の破棄フラグにかかわらず 195 処理されます。 196 """ 197 198 def __init__( 199 self, 200 dump_reason: bool = False, 201 p: float = 1, 202 skip_rejected: bool = False, 203 *args: Any, 204 **kwargs: Any, 205 ) -> None: 206 """ 207 Args: 208 dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False. 209 p (float, optional): Apply probability. Defaults to 1. 210 skip_rejected (bool, optional): 破棄済みサンプルを排除しません. 211 """ 212 super().__init__(p, skip_rejected, *args, **kwargs) 213 self.dump_reason = dump_reason 214 215 def apply(self, document: Document) -> Document: 216 """ 217 >>> JSONDumper()("hojichar") 218 '{"text": "hojichar"}' 219 """ 220 text = document.text 221 if self.dump_reason: 222 document.text = json.dumps( 223 { 224 "text": text, 225 "is_rejected": document.is_rejected, 226 "reason": document.reject_reason, 227 }, 228 ensure_ascii=False, 229 ) 230 else: 231 document.text = json.dumps({"text": text}, ensure_ascii=False) 232 return document 233 234 235class DocumentLengthFilter(Filter): 236 """ 237 `min_doc_len`, `max_doc_len` で指定した上限・下限の範囲内にないドキュメントを破棄します. 238 デフォルトでは 200字 以上 50000字以内のテキストが受理されます. 239 """ 240 241 def __init__( 242 self, 243 min_doc_len: Optional[int] = None, 244 max_doc_len: Optional[int] = None, 245 *args: Any, 246 **kwargs: Any, 247 ) -> None: 248 super().__init__(*args, **kwargs) 249 250 self.min_doc_len = min_doc_len 251 self.max_doc_len = max_doc_len 252 253 def apply(self, doc: Document) -> Document: 254 """ 255 >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected 256 True 257 """ 258 doc_len = len(doc.text) 259 if self.min_doc_len is not None: 260 if doc_len < self.min_doc_len: 261 doc.is_rejected = True 262 if self.max_doc_len is not None: 263 if self.max_doc_len < doc_len: 264 doc.is_rejected = True 265 return doc 266 267 268class NgWordsFilterJa(Filter): 269 """ 270 日本語のNGワード(および不適切語)を含む文書を破棄します. 271 `dict_path` で指定したファイルから, キーワードのリストを得ます. 272 ファイルは単語が改行で羅列されたテキストファイルです. 273 274 `ignore_confused` を `True` にすると, 275 偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます. 276 デフォルト値は `False` です. 277 """ 278 279 def __init__( 280 self, 281 dict_path: Union[str, PathLike], 282 ignore_confused: bool = False, 283 *args: Any, 284 **kwargs: Any, 285 ) -> None: 286 super().__init__(*args, **kwargs) 287 288 with open(dict_path, encoding="utf-8") as fp: 289 ng_words = fp.read().split("\n") 290 ng_words = [w.strip() for w in ng_words if not len(w) == 0] 291 292 if ignore_confused: 293 words_katakana = [] 294 words_not_katakana = [] 295 for w in ng_words: 296 if re.fullmatch(r"[ァ-ヴー]+", w): 297 words_katakana.append(re.escape(w)) 298 else: 299 words_not_katakana.append(re.escape(w)) 300 katakana_pat = "|".join(words_katakana) 301 katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])" 302 pat = "|".join(words_not_katakana) + "|" + katakana_pat 303 self.keyword_pat = re.compile(pat) 304 else: 305 ng_words = [re.escape(w) for w in ng_words] 306 pat = "|".join(ng_words) 307 self.keyword_pat = re.compile(pat) 308 309 def apply(self, doc: Document) -> Document: 310 regex_match = self.keyword_pat.search(doc.text) 311 if regex_match: 312 doc.is_rejected = True 313 self.matched_text = regex_match.group() 314 self.matched_text_neighbor = doc.text[ 315 regex_match.start() - 20 : regex_match.end() + 20 316 ] 317 318 return doc 319 320 321class NgWordsFilterEn(Filter): 322 """ 323 英語のNGワード(および不適切語)を含む文書を破棄します. 324 `dict_path` で指定したファイルから, キーワードのリストを得ます. 325 ファイルは単語が改行で羅列されたテキストファイルです. 326 """ 327 328 def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None: 329 super().__init__(*args, **kwargs) 330 331 with open(dict_path, encoding="utf-8") as fp: 332 ng_words = fp.read().split("\n") 333 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 334 pat = "|".join(ng_words) 335 # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ. 336 self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE) 337 338 def apply(self, doc: Document) -> Document: 339 if self.keyword_pat.search(doc.text): 340 doc.is_rejected = True 341 return doc 342 343 344class DiscardAdultContentJa(NgWordsFilterJa): 345 """ 346 日本語のアダルトキーワード(および不適切語)を含む文書を破棄します. 347 `dict_path` で指定したファイルから, キーワードのリストを得ます. 348 ファイルは単語が改行で羅列されたテキストファイルです. 349 デフォルトの`dict_path` は /hojichar/dict/adult_keywords_ja.txt です. 350 """ 351 352 def __init__( 353 self, 354 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt", 355 *args: Any, 356 **kwargs: Any, 357 ) -> None: 358 super().__init__(dict_path, *args, **kwargs) 359 360 def apply(self, doc: Document) -> Document: 361 """ 362 >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 363 True 364 365 >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected 366 False 367 368 挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック, 369 >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \ 370 # Matching with NG keyword "アス" 371 True 372 """ 373 return super().apply(doc) 374 375 376class DiscardAdultContentEn(NgWordsFilterEn): 377 """ 378 英語のアダルトキーワード(および不適切語)を含む文書を破棄します. 379 `dict_path` で指定したファイルから, キーワードのリストを得ます. 380 ファイルは単語が改行で羅列されたテキストファイルです. 381 デフォルトの`dict_path` は /hojichar/dict/adult_keywords_en.txt です. 382 """ 383 384 def __init__( 385 self, 386 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt", 387 *args: Any, 388 **kwargs: Any, 389 ) -> None: 390 super().__init__(dict_path, *args, **kwargs) 391 392 def apply(self, doc: Document) -> Document: 393 """ 394 >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 395 True 396 397 >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected 398 False 399 """ 400 return super().apply(doc) 401 402 403class DiscardDiscriminationContentJa(NgWordsFilterJa): 404 """ 405 日本語の差別キーワード(および不適切語)を含む文書を破棄します. 406 `dict_path` で指定したファイルから, キーワードのリストを得ます. 407 ファイルは単語が改行で羅列されたテキストファイルです. 408 デフォルトの`dict_path` は /hojichar/dict/discrimination_keywords_ja.txt です. 409 """ 410 411 def __init__( 412 self, 413 dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt", 414 *args: Any, 415 **kwargs: Any, 416 ): 417 super().__init__(dict_path, *args, **kwargs) 418 419 def apply(self, doc: Document) -> Document: 420 """ 421 >>> DiscardDiscriminationContentJa().\ 422 apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected 423 True 424 425 >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected 426 False 427 """ 428 return super().apply(doc) 429 430 431class DiscardViolenceContentJa(NgWordsFilterJa): 432 """ 433 日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します. 434 `dict_path` で指定したファイルから, キーワードのリストを得ます. 435 ファイルは単語が改行で羅列されたテキストファイルです. 436 デフォルトの`dict_path` は /hojichar/dict/violence_keywords_ja.txt です. 437 """ 438 439 def __init__( 440 self, 441 dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt", 442 *args: Any, 443 **kwargs: Any, 444 ) -> None: 445 super().__init__(dict_path, *args, **kwargs) 446 447 def apply(self, doc: Document) -> Document: 448 """ 449 >>> DiscardViolenceContentJa()\ 450 .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected 451 True 452 453 >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected 454 False 455 """ 456 return super().apply(doc) 457 458 459class DiscardBBSComments(Filter): 460 """ 461 正規表現 "BBS Pattern" に `max_allow_num` 回よりたくさんマッチする文書を破棄します. 462 `max_allow_num` のデフォルト値は14です. 463 正規表現 "BBS Pattern" は下記のリンクで検証可能です. 464 https://regex101.com/r/ybQvL2/1 465 """ 466 467 def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None: 468 super().__init__(*args, **kwargs) 469 470 self.max_allowed_num = max_allowed_num 471 self.keyword_pat = re.compile( 472 r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-" # noqa 473 ) 474 475 def apply(self, doc: Document) -> Document: 476 """ 477 >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected 478 True 479 480 >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected 481 False 482 """ 483 bbs_factor = self.keyword_pat.findall(doc.text) 484 if len(bbs_factor) > self.max_allowed_num: 485 doc.is_rejected = True 486 return doc 487 488 489class DiscardAds(Filter): 490 """ 491 主に広告キーワードを`max_allow_num`より多く含む文書を破棄します. 492 デフォルトで`max_allow_num` は14です. 493 `dict_path` で指定したファイルから, 広告キーワードのリストを得ます. 494 ファイルは単語が改行で羅列されたテキストファイルです. 495 デフォルトの`dict_path` は /hojichar/dict/advertisement_keywords_ja.txt です. 496 """ 497 498 def __init__( 499 self, 500 dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt", 501 max_allowed_num: int = 14, 502 *args: Any, 503 **kwargs: Any, 504 ): 505 super().__init__(*args, **kwargs) 506 507 self.max_allow_num = max_allowed_num 508 with open(dict_path, encoding="utf-8") as fp: 509 ng_words = fp.read().split("\n") 510 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 511 pat = r"|".join(ng_words) 512 self.keyword_pat = re.compile(pat) 513 514 def apply(self, doc: Document) -> Document: 515 """ 516 >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected 517 True 518 519 >>> DiscardAds().apply(Document("おはよう")).is_rejected 520 False 521 """ 522 ads_factor = self.keyword_pat.findall(doc.text) 523 if len(ads_factor) > self.max_allow_num: 524 doc.is_rejected = True 525 return doc 526 527 528class AcceptJapanese(Filter): 529 """ 530 日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます. 531 1. テキストを左から`lookup_size` (デフォルトで50字) 参照し, 532 ひらがな・カタカナが存在すれば日本語と判定する. 533 """ 534 535 def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None: 536 super().__init__(*args, **kwargs) 537 538 self.lookup_size = lookup_size 539 self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]") 540 541 def apply(self, doc: Document) -> Document: 542 """ 543 >>> AcceptJapanese().apply(Document("This is English document")).is_rejected 544 True 545 546 >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected 547 True 548 549 >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected 550 False 551 """ 552 if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]): 553 doc.is_rejected = True 554 return doc 555 556 557class DiscardRareKuten(Filter): 558 """ 559 日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます 560 ドキュメントを句点"。"で区切り, 平均文長が 561 `max_avarage_sentence_length` より長い場合は破棄します. 562 `max_avarage_sentence_length` のデフォルト値は100です. 563 このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します. 564 """ 565 566 def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None: 567 super().__init__(*args, **kwargs) 568 569 self.max_average_sentence_length = max_average_sentence_length 570 self.kuten_pat = re.compile(r"。") 571 572 def apply(self, doc: Document) -> Document: 573 """ 574 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected 575 False 576 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected 577 True 578 """ 579 kuten_lst = self.kuten_pat.findall(doc.text) 580 min_kuten_num = len(doc.text) / self.max_average_sentence_length 581 if len(kuten_lst) < min_kuten_num: 582 doc.is_rejected = True 583 return doc 584 585 586class HeaderFooterTagsRemover(Filter): 587 """ 588 ドキュメントの冒頭・末尾のトークンを調査し, ヘッダー・フッダー的な 589 タグが存在していた場合, そのトークンを除去します. 590 591 このフィルタを通す前に, 事前にセンテンスレベルにトーカナイズしておいてください. 592 このフィルタでは Document.token にのみ変更が加えられるので, 出力前 あるいは 下流フィルタで 593 Document.text に変更を加える前にトークンをマージしておいてください. 594 """ 595 596 def __init__( 597 self, 598 dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt", 599 *args: Any, 600 **kwargs: Any, 601 ) -> None: 602 super().__init__(*args, **kwargs) 603 604 with open(dict_path) as fp: 605 keywords = fp.read().split("\n") 606 keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0] 607 self.keyword_pat = re.compile(r"|".join(keywords)) 608 609 def apply(self, doc: Document) -> Document: 610 if len(doc.tokens) == 0: 611 return doc 612 613 lookup_size = 0 614 if 1 <= len(doc.tokens) < 4: 615 lookup_size = 1 616 elif 4 <= len(doc.tokens) < 6: 617 lookup_size = 2 618 elif 6 <= len(doc.tokens): 619 lookup_size = 3 620 621 for i in range(lookup_size): 622 if self.should_drop_token(doc.tokens[i]): 623 doc.tokens[i].is_rejected = True 624 if self.should_drop_token(doc.tokens[-(i + 1)]): 625 doc.tokens[i].is_rejected = True 626 627 return doc 628 629 def should_drop_token(self, token: Token) -> bool: 630 """ 631 >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>")) 632 True 633 634 >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶")) 635 False 636 637 Comment. 638 Original legacy code removed a pattern r"« _ | Main | _ »" . 639 In the pattern, "|" is not escaped, so **ANY** string was eliminated. 640 It seems unintended behavior, so I fix this. 641 """ 642 if self.keyword_pat.match(token.text): 643 return True 644 else: 645 return False 646 647 648class MaskPersonalInformation(Filter): 649 """ 650 ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします. 651 """ 652 653 def __init__(self, *args: Any, **kwargs: Any) -> None: 654 super().__init__(*args, **kwargs) 655 656 self.phone_pat = re.compile( 657 r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}" # noqa 658 ) 659 self.email_pat = re.compile( 660 r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)" # noqa 661 ) 662 663 def apply(self, doc: Document) -> Document: 664 """ 665 >>> MaskPersonalInformation()('06-1234-5678') 666 '06-1234-XXXX' 667 >>> MaskPersonalInformation()('075-123-4567') 668 '075-123-XXXX' 669 >>> MaskPersonalInformation()('0166-12-3456') 670 '0166-12-XXXX' 671 >>> MaskPersonalInformation()('09808-1-2345') 672 '09808-1-XXXX' 673 >>> MaskPersonalInformation()('090-1234-5678') 674 '090-1234-XXXX' 675 >>> MaskPersonalInformation()('0751234567') 676 '075123XXXX' 677 >>> MaskPersonalInformation()('08012345678') 678 '0801234XXXX' 679 >>> MaskPersonalInformation()('連絡は075-123-4567 まで') 680 '連絡は075-123-XXXX まで' 681 >>> MaskPersonalInformation()('+81-80-1234-5678') 682 '+81-80-1234-XXXX' 683 >>> MaskPersonalInformation()('+818012345678') 684 '+81801234XXXX' 685 >>> MaskPersonalInformation()('hogehoge@example.com') 686 'xxxx@yyy.com' 687 >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡') 688 '何かあれば xxxx@yyy.jp まで連絡' 689 """ 690 text = self.phone_pat.sub(r"\1XXXX", doc.text) 691 text = self.email_pat.sub(r"xxxx@yyy\1", text) 692 doc.text = text 693 return doc 694 695 696class DiscardTooManyNouns(Filter): 697 """ 698 [!CAUTION] This filter requires `fugashi` package. Please install it 699 by `pip install 'hojichar[all]'`. 700 701 A filter that removes document with too many nouns in Japanese i.e., 702 documents such as advertisement, word salad, etc ... 703 """ 704 705 def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None: 706 """ 707 Args: 708 threshold: document whose noun ratio is higher than this value will be discarded 709 *args: 710 **kwargs: 711 """ 712 super().__init__(*args, **kwargs) 713 assert ( 714 is_loaded_extras 715 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 716 717 self.threshold = threshold 718 self.tagger = Tagger("-Owakati") 719 assert ( 720 "unidic" in self.tagger.dictionary_info[0]["filename"] 721 ), "MeCab dictionary must be unidic" 722 723 def apply(self, doc: Document) -> Document: 724 """ 725 >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected 726 False 727 >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected 728 True 729 >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected 730 False 731 """ 732 # remove "補助記号" from part-of-speech statistics 733 # because they often decrease the noun ratio, 734 # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5 735 # however, we don't want such sentence 736 pos_count = Counter( 737 w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号" 738 ) 739 try: 740 noun_ratio = pos_count["名詞"] / sum(pos_count.values()) 741 except ZeroDivisionError: 742 noun_ratio = 0.0 743 if noun_ratio >= self.threshold: 744 doc.is_rejected = True 745 return doc 746 747 748class CharRepetitionRatioFilter(Filter): 749 """ 750 文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します. 751 名詞の連続からなるような広告テキストを取り除くのに有効です. 752 753 実装は, BigScience で採用されていた前処理を参考にしています. 754 元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453 # noqa: E501 755 756 「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが, 757 これは文書長の影響を軽減するためだとされています. 758 759 掲示板のテキストが引っかかりやすい傾向があります. 760 13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0 761 的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう 762 """ 763 764 def __init__( 765 self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any 766 ) -> None: 767 """ 768 769 Args: 770 threshold: document with character repetition ratio higher than this value will be discarded 771 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 772 *args: 773 **kwargs: 774 """ # noqa: E501 775 776 super().__init__(*args, **kwargs) 777 self.threshold = threshold 778 self.ngram_size = ngram_size 779 780 def apply(self, doc: Document) -> Document: 781 ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size) 782 if ratio >= self.threshold: 783 doc.is_rejected = True 784 return doc 785 786 @staticmethod 787 def compute_character_repetition_ratio( 788 document: str, character_repetition_length: int 789 ) -> float: 790 def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]: 791 character_ngrams: List[str] = [ 792 document[i : i + n] for i in range(len(document) - n + 1) 793 ] 794 freq_character_ngrams_dict: Dict[str, int] = {} 795 for character_ngram in character_ngrams: 796 freq_character_ngrams_dict[character_ngram] = ( 797 freq_character_ngrams_dict.get(character_ngram, 0) + 1 798 ) 799 return freq_character_ngrams_dict 800 801 freq_character_ngrams_dict = get_freq_character_ngrams( 802 document, character_repetition_length 803 ) 804 if len(freq_character_ngrams_dict) == 0: 805 return 0.0 806 freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values()) 807 freq_character_ngrams = sorted(freq_character_ngrams, reverse=True) 808 val_one = len([el for el in freq_character_ngrams if el == 1]) 809 num_rep_character_ngrams = min( 810 int(np.sqrt(len(freq_character_ngrams))), 811 len(freq_character_ngrams) - val_one, 812 ) 813 character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum( 814 freq_character_ngrams 815 ) 816 return character_repetition_ratio 817 818 819class WordRepetitionRatioFilter(Filter): 820 """ 821 [!CAUTION] This filter requires `fugashi` package. Please install it 822 by `pip install 'hojichar[all]'`. 823 824 単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ. 825 BigScienceで採用されていた前処理を参考にしている. 826 827 名詞が連打されているような広告テキストを取り除くのに有効な様子 828 まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない 829 例: 830 "ウェブ\n本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ)\n2013/05/10(10:57) 831 ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ 832 られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる 833 なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上 834 高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら 835 経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時 836 56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ)\n2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄 837 り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入 838 るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増 839 益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、 840 電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ 841 イの回復で収益が急回" 842 """ # noqa: E501 843 844 def __init__( 845 self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any 846 ) -> None: 847 """ 848 849 Args: 850 threshold: document whose character repetition ratio is higher than this value will be discarded 851 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 852 *args: 853 **kwargs: 854 """ # noqa: E501 855 super().__init__(*args, **kwargs) 856 assert ( 857 is_loaded_extras 858 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 859 860 self.threshold = threshold 861 self.ngram_size = ngram_size 862 self.tagger = Tagger("-Owakati") 863 864 def apply(self, doc: Document) -> Document: 865 ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size) 866 if ratio >= self.threshold: 867 doc.is_rejected = True 868 return doc 869 870 def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float: 871 def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]: 872 # tokenizing given document 873 words = [w.surface for w in self.tagger(document)] 874 word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)] 875 freq_word_ngrams: Dict[str, int] = {} 876 for word_ngram in word_ngrams: 877 freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1 878 return freq_word_ngrams 879 880 freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length) 881 if len(freq_word_ngrams_dict) == 0: 882 return 0 883 freq_word_ngrams = list(freq_word_ngrams_dict.values()) 884 word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum( 885 freq_word_ngrams 886 ) 887 888 return word_repetition_ratio 889 890 891class DiscardTooManySpecialToken(Filter): 892 """ 893 [!CAUTION] This filter requires `emoji` package. Please install it 894 by `pip install 'hojichar[all]'`. 895 896 句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ 897 元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16 # noqa: E501 898 """ 899 900 def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None: 901 """ 902 903 Args: 904 threshold: document whose special token ratio is higher than this value will be discarded 905 *args: 906 **kwargs: 907 """ # noqa: E501 908 super().__init__(*args, **kwargs) 909 910 # digits are not regarded as special tokens 911 # otherwise many false positives are made, i.e., good documents discarded 912 main_special_characters = string.punctuation + string.whitespace # + string.digits 913 other_special_characters = ( 914 " ’“”–▬…✦�£•€«»°·═" 915 "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰ ‑≤≥‖" 916 "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚" 917 "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖" 918 "」﴾》�" 919 ) 920 921 en_emoji = emoji.EMOJI_DATA.keys() 922 923 special_characters_default = set(main_special_characters + other_special_characters) 924 special_characters_default.update(en_emoji) 925 self.special_characters = special_characters_default 926 927 self.threshold = threshold 928 929 def _compute_special_characters_ratio(self, text: str) -> float: 930 if len(text) == 0: 931 return 0 932 933 special_characters_ratio = len( 934 [char for char in text if char in self.special_characters] 935 ) / len(text) 936 return special_characters_ratio 937 938 def apply(self, doc: Document) -> Document: 939 special_characters_ratio = self._compute_special_characters_ratio(doc.text) 940 941 if special_characters_ratio > self.threshold: 942 doc.is_rejected = True 943 return doc 944 945 946class SingleCharacterRepetitionFilter(Filter): 947 """ 948 単一文字が大量に繰り返されているような文書を取り除くためのフィルタ 949 そのような文書はノイズである可能性が高いため 950 参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい 951 https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset # noqa: E501 952 """ 953 954 def __init__( 955 self, 956 threshold: int = 200, 957 *args: Any, 958 **kwargs: Any, 959 ) -> None: 960 """ 961 Args: 962 threshold: The document is removed if character is repeated for this value or more 963 *args: 964 **kwargs: 965 """ 966 super().__init__(*args, **kwargs) 967 self.threshold = threshold 968 969 def _is_repeat_contained(self, text: str) -> bool: 970 groups = groupby(text) 971 is_repeat_contained = any(sum(1 for _ in group) >= self.threshold for _, group in groups) 972 return is_repeat_contained 973 974 def apply(self, doc: Document) -> Document: 975 if self._is_repeat_contained(doc.text): 976 doc.is_rejected = True 977 return doc 978 979 980class DiscardTooManyEndingEllipsis(Filter): 981 """ 982 ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです. 983 ellipsisとしては ... と … を用いている 984 同様のフィルタが RedPajama v2で用いられています. 985 986 例として, 以下のような文書を検知します. 987 ``` 988 ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付... 989 バツイチアラフォー 婚活ち女性の特徴と子持な付... 990 ``` 991 992 デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、 993 precisionを重視した設定です. 994 """ 995 996 def __init__( 997 self, 998 threshold: float = 0.7, 999 *args: Any, 1000 **kwargs: Any, 1001 ) -> None: 1002 """ 1003 Args: 1004 threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value 1005 *args: 1006 **kwargs: 1007 """ # noqa: E501 1008 super().__init__(*args, **kwargs) 1009 self.threshold = threshold 1010 self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n") # matches ...\n and …\n 1011 1012 def apply(self, doc: Document) -> Document: 1013 ellipsis_count = len(self.ellipsis_pattern.findall(doc.text)) 1014 newline_count = max(doc.text.count("\n"), 1) # avoid zero division 1015 ellipsis_ratio = ellipsis_count / newline_count 1016 1017 if ellipsis_ratio > self.threshold: 1018 doc.is_rejected = True 1019 return doc 1020 1021 1022class DiscardTooShortLines(Filter): 1023 """ 1024 短い行を大量に含む文書を捨てるためのフィルタです. 1025 1026 メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です. 1027 """ 1028 1029 def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None: 1030 """ 1031 Args: 1032 threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value. 1033 *args: 1034 **kwargs: 1035 """ # noqa: E501 1036 super().__init__(*args, **kwargs) 1037 self.threshold = threshold 1038 # この値は適当に決め打ち 1039 self.minimum_line_length = 10 1040 1041 def apply(self, doc: Document) -> Document: 1042 lines = [len(x) for x in doc.text.split("\n")] 1043 short_lines = [x for x in lines if x <= self.minimum_line_length] 1044 if (len(short_lines) / len(lines)) > self.threshold: 1045 doc.is_rejected = True 1046 return doc
32class ExampleHojiChar(Filter): 33 """基本的なフィルタの実装例です. 末尾に'<hojichar>'を追加します.""" 34 35 def apply(self, document: Document) -> Document: 36 """ 37 >>> ExampleHojiChar()("hello, world") 38 'hello, world<hojichar>' 39 """ 40 document.text += "<hojichar>" 41 return document
基本的なフィルタの実装例です. 末尾に'
35 def apply(self, document: Document) -> Document: 36 """ 37 >>> ExampleHojiChar()("hello, world") 38 'hello, world<hojichar>' 39 """ 40 document.text += "<hojichar>" 41 return document
>>> ExampleHojiChar()("hello, world")
'hello, world<hojichar>'
Inherited Members
44class ExampleDiscardDocumentContainKeyword(Filter): 45 """特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です.""" 46 47 def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None: 48 super().__init__(*args, **kwargs) 49 self.keyword = keyword 50 51 def apply(self, document: Document) -> Document: 52 """ 53 >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected 54 True 55 """ 56 if self.keyword in document.text: 57 document.is_rejected = True 58 return document
特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です.
47 def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None: 48 super().__init__(*args, **kwargs) 49 self.keyword = keyword
Parameters
p : float, optional Probability that this filter will be applied. Default=1
51 def apply(self, document: Document) -> Document: 52 """ 53 >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected 54 True 55 """ 56 if self.keyword in document.text: 57 document.is_rejected = True 58 return document
>>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected
True
Inherited Members
61class Identity(Filter): 62 """何も変化を加えないフィルタです. テスト・デバッグに用いられます.""" 63 64 def apply(self, document: Document) -> Document: 65 return document
何も変化を加えないフィルタです. テスト・デバッグに用いられます.
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
68class DiscardAll(Filter): 69 """ 70 すべてのドキュメントを破棄するフィルタです. 71 テスト・デバッグに用いられます. 72 """ 73 74 def apply(self, document: Document) -> Document: 75 document.is_rejected = True 76 return document
すべてのドキュメントを破棄するフィルタです. テスト・デバッグに用いられます.
74 def apply(self, document: Document) -> Document: 75 document.is_rejected = True 76 return document
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
79class ApplyDiscard(Filter): 80 """ 81 上流フィルタで破棄された`Document`を空文字列にします. 82 83 `Document.is_rejected=True` の ドキュメントは無視されるため, 84 このフィルタを `Compose` のコンストラクタに渡しても動作しません. 85 このフィルタは主に`Compose` 内部や, `discard_filtered=False` を指定 86 したデバッグ時などに利用されます. 87 """ 88 89 def __init__(self, *args: Any, **kwargs: Any) -> None: 90 super().__init__(*args, **kwargs) 91 92 def apply(self, document: Document) -> Document: 93 """ 94 >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text 95 '' 96 """ 97 if document.is_rejected: 98 document.text = "" 99 100 return document
上流フィルタで破棄されたDocument
を空文字列にします.
Document.is_rejected=True
の ドキュメントは無視されるため,
このフィルタを Compose
のコンストラクタに渡しても動作しません.
このフィルタは主にCompose
内部や, discard_filtered=False
を指定
したデバッグ時などに利用されます.
Parameters
p : float, optional Probability that this filter will be applied. Default=1
92 def apply(self, document: Document) -> Document: 93 """ 94 >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text 95 '' 96 """ 97 if document.is_rejected: 98 document.text = "" 99 100 return document
>>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text
''
Inherited Members
103class Sleep(Filter): 104 """ 105 デバッグ用のフィルタです. 指定秒スリープします. 106 """ 107 108 def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None: 109 super().__init__(*args, **kwargs) 110 self.time = time 111 112 def apply(self, document: Document) -> Document: 113 """ 114 >>> Sleep(0.1)('hello') # After 0.1 seconds, 115 'hello' 116 """ 117 time.sleep(self.time) 118 return document
デバッグ用のフィルタです. 指定秒スリープします.
108 def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None: 109 super().__init__(*args, **kwargs) 110 self.time = time
Parameters
p : float, optional Probability that this filter will be applied. Default=1
112 def apply(self, document: Document) -> Document: 113 """ 114 >>> Sleep(0.1)('hello') # After 0.1 seconds, 115 'hello' 116 """ 117 time.sleep(self.time) 118 return document
>>> Sleep(0.1)('hello') # After 0.1 seconds,
'hello'
Inherited Members
121class DocumentNormalizer(Filter): 122 """ 123 Unicode の正規化をします. 124 """ 125 126 def __init__(self, *args: Any, **kwargs: Any) -> None: 127 super().__init__(*args, **kwargs) 128 129 def apply(self, document: Document) -> Document: 130 document.text = unicodedata.normalize("NFKC", document.text) 131 return document
Unicode の正規化をします.
Parameters
p : float, optional Probability that this filter will be applied. Default=1
129 def apply(self, document: Document) -> Document: 130 document.text = unicodedata.normalize("NFKC", document.text) 131 return document
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
134class JSONLoader(Filter): 135 """ 136 テキストを Json として解釈し, `key` で指定した要素を文字列として 137 doument に格納します.デフォルトの `key` は 'text' です. 138 139 Json の読み込み, あるいは `key` の読み込みに失敗した際には例外を送出します. 140 これらを無視する場合は, `ignore=True` にします. その際, 読み込みに失敗 141 したドキュメントは破棄されます. 142 """ 143 144 def __init__( 145 self, 146 key: str = "text", 147 ignore: bool = False, 148 extra_keys: Optional[List[str]] = None, 149 *args: Any, 150 **kwargs: Any, 151 ) -> None: 152 super().__init__(*args, **kwargs) 153 self.key = key 154 self.ignore = ignore 155 self.extra_keys = extra_keys 156 157 def apply(self, document: Document) -> Document: 158 """ 159 >>> JSONLoader()( '{"text": "hello, world", "words": 2}' ) 160 'hello, world' 161 162 >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON 163 Traceback (most recent call last): 164 ... 165 json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9) 166 167 >>> JSONLoader()( '{"words": 2}' ) 168 Traceback (most recent call last): 169 ... 170 KeyError: 'text' 171 172 >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected 173 True 174 """ 175 try: 176 data = json.loads(document.text) 177 document.text = str(data[self.key]) 178 if self.extra_keys is not None: 179 document.extras = {key: data[key] for key in self.extra_keys if key in data} 180 except Exception as e: 181 logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}") 182 if self.ignore: 183 document.is_rejected = True 184 return document 185 else: 186 raise e 187 188 return document
テキストを Json として解釈し, key
で指定した要素を文字列として
doument に格納します.デフォルトの key
は 'text' です.
Json の読み込み, あるいは key
の読み込みに失敗した際には例外を送出します.
これらを無視する場合は, ignore=True
にします. その際, 読み込みに失敗
したドキュメントは破棄されます.
144 def __init__( 145 self, 146 key: str = "text", 147 ignore: bool = False, 148 extra_keys: Optional[List[str]] = None, 149 *args: Any, 150 **kwargs: Any, 151 ) -> None: 152 super().__init__(*args, **kwargs) 153 self.key = key 154 self.ignore = ignore 155 self.extra_keys = extra_keys
Parameters
p : float, optional Probability that this filter will be applied. Default=1
157 def apply(self, document: Document) -> Document: 158 """ 159 >>> JSONLoader()( '{"text": "hello, world", "words": 2}' ) 160 'hello, world' 161 162 >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON 163 Traceback (most recent call last): 164 ... 165 json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9) 166 167 >>> JSONLoader()( '{"words": 2}' ) 168 Traceback (most recent call last): 169 ... 170 KeyError: 'text' 171 172 >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected 173 True 174 """ 175 try: 176 data = json.loads(document.text) 177 document.text = str(data[self.key]) 178 if self.extra_keys is not None: 179 document.extras = {key: data[key] for key in self.extra_keys if key in data} 180 except Exception as e: 181 logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}") 182 if self.ignore: 183 document.is_rejected = True 184 return document 185 else: 186 raise e 187 188 return document
>>> JSONLoader()( '{"text": "hello, world", "words": 2}' )
'hello, world'
>>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON
Traceback (most recent call last):
...
json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9)
>>> JSONLoader()( '{"words": 2}' )
Traceback (most recent call last):
...
KeyError: 'text'
>>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected
True
Inherited Members
191class JSONDumper(Filter): 192 """ 193 Document.text の文字列を json に変換します. 194 必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。 195 デフォルトで `skip_rejected` が `False` にセットされており、Document の破棄フラグにかかわらず 196 処理されます。 197 """ 198 199 def __init__( 200 self, 201 dump_reason: bool = False, 202 p: float = 1, 203 skip_rejected: bool = False, 204 *args: Any, 205 **kwargs: Any, 206 ) -> None: 207 """ 208 Args: 209 dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False. 210 p (float, optional): Apply probability. Defaults to 1. 211 skip_rejected (bool, optional): 破棄済みサンプルを排除しません. 212 """ 213 super().__init__(p, skip_rejected, *args, **kwargs) 214 self.dump_reason = dump_reason 215 216 def apply(self, document: Document) -> Document: 217 """ 218 >>> JSONDumper()("hojichar") 219 '{"text": "hojichar"}' 220 """ 221 text = document.text 222 if self.dump_reason: 223 document.text = json.dumps( 224 { 225 "text": text, 226 "is_rejected": document.is_rejected, 227 "reason": document.reject_reason, 228 }, 229 ensure_ascii=False, 230 ) 231 else: 232 document.text = json.dumps({"text": text}, ensure_ascii=False) 233 return document
Document.text の文字列を json に変換します.
必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。
デフォルトで skip_rejected
が False
にセットされており、Document の破棄フラグにかかわらず
処理されます。
199 def __init__( 200 self, 201 dump_reason: bool = False, 202 p: float = 1, 203 skip_rejected: bool = False, 204 *args: Any, 205 **kwargs: Any, 206 ) -> None: 207 """ 208 Args: 209 dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False. 210 p (float, optional): Apply probability. Defaults to 1. 211 skip_rejected (bool, optional): 破棄済みサンプルを排除しません. 212 """ 213 super().__init__(p, skip_rejected, *args, **kwargs) 214 self.dump_reason = dump_reason
Args:
dump_reason (bool, optional): is_rejected
, reason
エントリをダンプします. Defaults to False.
p (float, optional): Apply probability. Defaults to 1.
skip_rejected (bool, optional): 破棄済みサンプルを排除しません.
216 def apply(self, document: Document) -> Document: 217 """ 218 >>> JSONDumper()("hojichar") 219 '{"text": "hojichar"}' 220 """ 221 text = document.text 222 if self.dump_reason: 223 document.text = json.dumps( 224 { 225 "text": text, 226 "is_rejected": document.is_rejected, 227 "reason": document.reject_reason, 228 }, 229 ensure_ascii=False, 230 ) 231 else: 232 document.text = json.dumps({"text": text}, ensure_ascii=False) 233 return document
>>> JSONDumper()("hojichar")
'{"text": "hojichar"}'
Inherited Members
236class DocumentLengthFilter(Filter): 237 """ 238 `min_doc_len`, `max_doc_len` で指定した上限・下限の範囲内にないドキュメントを破棄します. 239 デフォルトでは 200字 以上 50000字以内のテキストが受理されます. 240 """ 241 242 def __init__( 243 self, 244 min_doc_len: Optional[int] = None, 245 max_doc_len: Optional[int] = None, 246 *args: Any, 247 **kwargs: Any, 248 ) -> None: 249 super().__init__(*args, **kwargs) 250 251 self.min_doc_len = min_doc_len 252 self.max_doc_len = max_doc_len 253 254 def apply(self, doc: Document) -> Document: 255 """ 256 >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected 257 True 258 """ 259 doc_len = len(doc.text) 260 if self.min_doc_len is not None: 261 if doc_len < self.min_doc_len: 262 doc.is_rejected = True 263 if self.max_doc_len is not None: 264 if self.max_doc_len < doc_len: 265 doc.is_rejected = True 266 return doc
min_doc_len
, max_doc_len
で指定した上限・下限の範囲内にないドキュメントを破棄します.
デフォルトでは 200字 以上 50000字以内のテキストが受理されます.
242 def __init__( 243 self, 244 min_doc_len: Optional[int] = None, 245 max_doc_len: Optional[int] = None, 246 *args: Any, 247 **kwargs: Any, 248 ) -> None: 249 super().__init__(*args, **kwargs) 250 251 self.min_doc_len = min_doc_len 252 self.max_doc_len = max_doc_len
Parameters
p : float, optional Probability that this filter will be applied. Default=1
254 def apply(self, doc: Document) -> Document: 255 """ 256 >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected 257 True 258 """ 259 doc_len = len(doc.text) 260 if self.min_doc_len is not None: 261 if doc_len < self.min_doc_len: 262 doc.is_rejected = True 263 if self.max_doc_len is not None: 264 if self.max_doc_len < doc_len: 265 doc.is_rejected = True 266 return doc
>>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
True
Inherited Members
269class NgWordsFilterJa(Filter): 270 """ 271 日本語のNGワード(および不適切語)を含む文書を破棄します. 272 `dict_path` で指定したファイルから, キーワードのリストを得ます. 273 ファイルは単語が改行で羅列されたテキストファイルです. 274 275 `ignore_confused` を `True` にすると, 276 偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます. 277 デフォルト値は `False` です. 278 """ 279 280 def __init__( 281 self, 282 dict_path: Union[str, PathLike], 283 ignore_confused: bool = False, 284 *args: Any, 285 **kwargs: Any, 286 ) -> None: 287 super().__init__(*args, **kwargs) 288 289 with open(dict_path, encoding="utf-8") as fp: 290 ng_words = fp.read().split("\n") 291 ng_words = [w.strip() for w in ng_words if not len(w) == 0] 292 293 if ignore_confused: 294 words_katakana = [] 295 words_not_katakana = [] 296 for w in ng_words: 297 if re.fullmatch(r"[ァ-ヴー]+", w): 298 words_katakana.append(re.escape(w)) 299 else: 300 words_not_katakana.append(re.escape(w)) 301 katakana_pat = "|".join(words_katakana) 302 katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])" 303 pat = "|".join(words_not_katakana) + "|" + katakana_pat 304 self.keyword_pat = re.compile(pat) 305 else: 306 ng_words = [re.escape(w) for w in ng_words] 307 pat = "|".join(ng_words) 308 self.keyword_pat = re.compile(pat) 309 310 def apply(self, doc: Document) -> Document: 311 regex_match = self.keyword_pat.search(doc.text) 312 if regex_match: 313 doc.is_rejected = True 314 self.matched_text = regex_match.group() 315 self.matched_text_neighbor = doc.text[ 316 regex_match.start() - 20 : regex_match.end() + 20 317 ] 318 319 return doc
日本語のNGワード(および不適切語)を含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
ignore_confused
を True
にすると,
偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます.
デフォルト値は False
です.
280 def __init__( 281 self, 282 dict_path: Union[str, PathLike], 283 ignore_confused: bool = False, 284 *args: Any, 285 **kwargs: Any, 286 ) -> None: 287 super().__init__(*args, **kwargs) 288 289 with open(dict_path, encoding="utf-8") as fp: 290 ng_words = fp.read().split("\n") 291 ng_words = [w.strip() for w in ng_words if not len(w) == 0] 292 293 if ignore_confused: 294 words_katakana = [] 295 words_not_katakana = [] 296 for w in ng_words: 297 if re.fullmatch(r"[ァ-ヴー]+", w): 298 words_katakana.append(re.escape(w)) 299 else: 300 words_not_katakana.append(re.escape(w)) 301 katakana_pat = "|".join(words_katakana) 302 katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])" 303 pat = "|".join(words_not_katakana) + "|" + katakana_pat 304 self.keyword_pat = re.compile(pat) 305 else: 306 ng_words = [re.escape(w) for w in ng_words] 307 pat = "|".join(ng_words) 308 self.keyword_pat = re.compile(pat)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
310 def apply(self, doc: Document) -> Document: 311 regex_match = self.keyword_pat.search(doc.text) 312 if regex_match: 313 doc.is_rejected = True 314 self.matched_text = regex_match.group() 315 self.matched_text_neighbor = doc.text[ 316 regex_match.start() - 20 : regex_match.end() + 20 317 ] 318 319 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
322class NgWordsFilterEn(Filter): 323 """ 324 英語のNGワード(および不適切語)を含む文書を破棄します. 325 `dict_path` で指定したファイルから, キーワードのリストを得ます. 326 ファイルは単語が改行で羅列されたテキストファイルです. 327 """ 328 329 def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None: 330 super().__init__(*args, **kwargs) 331 332 with open(dict_path, encoding="utf-8") as fp: 333 ng_words = fp.read().split("\n") 334 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 335 pat = "|".join(ng_words) 336 # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ. 337 self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE) 338 339 def apply(self, doc: Document) -> Document: 340 if self.keyword_pat.search(doc.text): 341 doc.is_rejected = True 342 return doc
英語のNGワード(および不適切語)を含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
329 def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None: 330 super().__init__(*args, **kwargs) 331 332 with open(dict_path, encoding="utf-8") as fp: 333 ng_words = fp.read().split("\n") 334 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 335 pat = "|".join(ng_words) 336 # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ. 337 self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
339 def apply(self, doc: Document) -> Document: 340 if self.keyword_pat.search(doc.text): 341 doc.is_rejected = True 342 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
345class DiscardAdultContentJa(NgWordsFilterJa): 346 """ 347 日本語のアダルトキーワード(および不適切語)を含む文書を破棄します. 348 `dict_path` で指定したファイルから, キーワードのリストを得ます. 349 ファイルは単語が改行で羅列されたテキストファイルです. 350 デフォルトの`dict_path` は /hojichar/dict/adult_keywords_ja.txt です. 351 """ 352 353 def __init__( 354 self, 355 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt", 356 *args: Any, 357 **kwargs: Any, 358 ) -> None: 359 super().__init__(dict_path, *args, **kwargs) 360 361 def apply(self, doc: Document) -> Document: 362 """ 363 >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 364 True 365 366 >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected 367 False 368 369 挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック, 370 >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \ 371 # Matching with NG keyword "アス" 372 True 373 """ 374 return super().apply(doc)
日本語のアダルトキーワード(および不適切語)を含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
デフォルトのdict_path
は /hojichar/dict/adult_keywords_ja.txt です.
353 def __init__( 354 self, 355 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt", 356 *args: Any, 357 **kwargs: Any, 358 ) -> None: 359 super().__init__(dict_path, *args, **kwargs)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
361 def apply(self, doc: Document) -> Document: 362 """ 363 >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 364 True 365 366 >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected 367 False 368 369 挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック, 370 >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \ 371 # Matching with NG keyword "アス" 372 True 373 """ 374 return super().apply(doc)
>>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
True
>>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
False
挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
>>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected # Matching with NG keyword "アス"
True
Inherited Members
377class DiscardAdultContentEn(NgWordsFilterEn): 378 """ 379 英語のアダルトキーワード(および不適切語)を含む文書を破棄します. 380 `dict_path` で指定したファイルから, キーワードのリストを得ます. 381 ファイルは単語が改行で羅列されたテキストファイルです. 382 デフォルトの`dict_path` は /hojichar/dict/adult_keywords_en.txt です. 383 """ 384 385 def __init__( 386 self, 387 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt", 388 *args: Any, 389 **kwargs: Any, 390 ) -> None: 391 super().__init__(dict_path, *args, **kwargs) 392 393 def apply(self, doc: Document) -> Document: 394 """ 395 >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 396 True 397 398 >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected 399 False 400 """ 401 return super().apply(doc)
英語のアダルトキーワード(および不適切語)を含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
デフォルトのdict_path
は /hojichar/dict/adult_keywords_en.txt です.
385 def __init__( 386 self, 387 dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt", 388 *args: Any, 389 **kwargs: Any, 390 ) -> None: 391 super().__init__(dict_path, *args, **kwargs)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
393 def apply(self, doc: Document) -> Document: 394 """ 395 >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected 396 True 397 398 >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected 399 False 400 """ 401 return super().apply(doc)
>>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
True
>>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
False
Inherited Members
404class DiscardDiscriminationContentJa(NgWordsFilterJa): 405 """ 406 日本語の差別キーワード(および不適切語)を含む文書を破棄します. 407 `dict_path` で指定したファイルから, キーワードのリストを得ます. 408 ファイルは単語が改行で羅列されたテキストファイルです. 409 デフォルトの`dict_path` は /hojichar/dict/discrimination_keywords_ja.txt です. 410 """ 411 412 def __init__( 413 self, 414 dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt", 415 *args: Any, 416 **kwargs: Any, 417 ): 418 super().__init__(dict_path, *args, **kwargs) 419 420 def apply(self, doc: Document) -> Document: 421 """ 422 >>> DiscardDiscriminationContentJa().\ 423 apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected 424 True 425 426 >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected 427 False 428 """ 429 return super().apply(doc)
日本語の差別キーワード(および不適切語)を含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
デフォルトのdict_path
は /hojichar/dict/discrimination_keywords_ja.txt です.
412 def __init__( 413 self, 414 dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt", 415 *args: Any, 416 **kwargs: Any, 417 ): 418 super().__init__(dict_path, *args, **kwargs)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
420 def apply(self, doc: Document) -> Document: 421 """ 422 >>> DiscardDiscriminationContentJa().\ 423 apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected 424 True 425 426 >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected 427 False 428 """ 429 return super().apply(doc)
>>> DiscardDiscriminationContentJa(). apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
True
>>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
False
Inherited Members
432class DiscardViolenceContentJa(NgWordsFilterJa): 433 """ 434 日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します. 435 `dict_path` で指定したファイルから, キーワードのリストを得ます. 436 ファイルは単語が改行で羅列されたテキストファイルです. 437 デフォルトの`dict_path` は /hojichar/dict/violence_keywords_ja.txt です. 438 """ 439 440 def __init__( 441 self, 442 dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt", 443 *args: Any, 444 **kwargs: Any, 445 ) -> None: 446 super().__init__(dict_path, *args, **kwargs) 447 448 def apply(self, doc: Document) -> Document: 449 """ 450 >>> DiscardViolenceContentJa()\ 451 .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected 452 True 453 454 >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected 455 False 456 """ 457 return super().apply(doc)
日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します.
dict_path
で指定したファイルから, キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
デフォルトのdict_path
は /hojichar/dict/violence_keywords_ja.txt です.
440 def __init__( 441 self, 442 dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt", 443 *args: Any, 444 **kwargs: Any, 445 ) -> None: 446 super().__init__(dict_path, *args, **kwargs)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
448 def apply(self, doc: Document) -> Document: 449 """ 450 >>> DiscardViolenceContentJa()\ 451 .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected 452 True 453 454 >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected 455 False 456 """ 457 return super().apply(doc)
>>> DiscardViolenceContentJa() .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
True
>>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
False
Inherited Members
460class DiscardBBSComments(Filter): 461 """ 462 正規表現 "BBS Pattern" に `max_allow_num` 回よりたくさんマッチする文書を破棄します. 463 `max_allow_num` のデフォルト値は14です. 464 正規表現 "BBS Pattern" は下記のリンクで検証可能です. 465 https://regex101.com/r/ybQvL2/1 466 """ 467 468 def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None: 469 super().__init__(*args, **kwargs) 470 471 self.max_allowed_num = max_allowed_num 472 self.keyword_pat = re.compile( 473 r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-" # noqa 474 ) 475 476 def apply(self, doc: Document) -> Document: 477 """ 478 >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected 479 True 480 481 >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected 482 False 483 """ 484 bbs_factor = self.keyword_pat.findall(doc.text) 485 if len(bbs_factor) > self.max_allowed_num: 486 doc.is_rejected = True 487 return doc
正規表現 "BBS Pattern" に max_allow_num
回よりたくさんマッチする文書を破棄します.
max_allow_num
のデフォルト値は14です.
正規表現 "BBS Pattern" は下記のリンクで検証可能です.
https://regex101.com/r/ybQvL2/1
468 def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None: 469 super().__init__(*args, **kwargs) 470 471 self.max_allowed_num = max_allowed_num 472 self.keyword_pat = re.compile( 473 r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-" # noqa 474 )
Parameters
p : float, optional Probability that this filter will be applied. Default=1
476 def apply(self, doc: Document) -> Document: 477 """ 478 >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected 479 True 480 481 >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected 482 False 483 """ 484 bbs_factor = self.keyword_pat.findall(doc.text) 485 if len(bbs_factor) > self.max_allowed_num: 486 doc.is_rejected = True 487 return doc
>>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
True
>>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
False
Inherited Members
490class DiscardAds(Filter): 491 """ 492 主に広告キーワードを`max_allow_num`より多く含む文書を破棄します. 493 デフォルトで`max_allow_num` は14です. 494 `dict_path` で指定したファイルから, 広告キーワードのリストを得ます. 495 ファイルは単語が改行で羅列されたテキストファイルです. 496 デフォルトの`dict_path` は /hojichar/dict/advertisement_keywords_ja.txt です. 497 """ 498 499 def __init__( 500 self, 501 dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt", 502 max_allowed_num: int = 14, 503 *args: Any, 504 **kwargs: Any, 505 ): 506 super().__init__(*args, **kwargs) 507 508 self.max_allow_num = max_allowed_num 509 with open(dict_path, encoding="utf-8") as fp: 510 ng_words = fp.read().split("\n") 511 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 512 pat = r"|".join(ng_words) 513 self.keyword_pat = re.compile(pat) 514 515 def apply(self, doc: Document) -> Document: 516 """ 517 >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected 518 True 519 520 >>> DiscardAds().apply(Document("おはよう")).is_rejected 521 False 522 """ 523 ads_factor = self.keyword_pat.findall(doc.text) 524 if len(ads_factor) > self.max_allow_num: 525 doc.is_rejected = True 526 return doc
主に広告キーワードをmax_allow_num
より多く含む文書を破棄します.
デフォルトでmax_allow_num
は14です.
dict_path
で指定したファイルから, 広告キーワードのリストを得ます.
ファイルは単語が改行で羅列されたテキストファイルです.
デフォルトのdict_path
は /hojichar/dict/advertisement_keywords_ja.txt です.
499 def __init__( 500 self, 501 dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt", 502 max_allowed_num: int = 14, 503 *args: Any, 504 **kwargs: Any, 505 ): 506 super().__init__(*args, **kwargs) 507 508 self.max_allow_num = max_allowed_num 509 with open(dict_path, encoding="utf-8") as fp: 510 ng_words = fp.read().split("\n") 511 ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0] 512 pat = r"|".join(ng_words) 513 self.keyword_pat = re.compile(pat)
Parameters
p : float, optional Probability that this filter will be applied. Default=1
515 def apply(self, doc: Document) -> Document: 516 """ 517 >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected 518 True 519 520 >>> DiscardAds().apply(Document("おはよう")).is_rejected 521 False 522 """ 523 ads_factor = self.keyword_pat.findall(doc.text) 524 if len(ads_factor) > self.max_allow_num: 525 doc.is_rejected = True 526 return doc
>>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
True
>>> DiscardAds().apply(Document("おはよう")).is_rejected
False
Inherited Members
529class AcceptJapanese(Filter): 530 """ 531 日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます. 532 1. テキストを左から`lookup_size` (デフォルトで50字) 参照し, 533 ひらがな・カタカナが存在すれば日本語と判定する. 534 """ 535 536 def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None: 537 super().__init__(*args, **kwargs) 538 539 self.lookup_size = lookup_size 540 self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]") 541 542 def apply(self, doc: Document) -> Document: 543 """ 544 >>> AcceptJapanese().apply(Document("This is English document")).is_rejected 545 True 546 547 >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected 548 True 549 550 >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected 551 False 552 """ 553 if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]): 554 doc.is_rejected = True 555 return doc
日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます.
1. テキストを左からlookup_size
(デフォルトで50字) 参照し,
ひらがな・カタカナが存在すれば日本語と判定する.
536 def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None: 537 super().__init__(*args, **kwargs) 538 539 self.lookup_size = lookup_size 540 self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]")
Parameters
p : float, optional Probability that this filter will be applied. Default=1
542 def apply(self, doc: Document) -> Document: 543 """ 544 >>> AcceptJapanese().apply(Document("This is English document")).is_rejected 545 True 546 547 >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected 548 True 549 550 >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected 551 False 552 """ 553 if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]): 554 doc.is_rejected = True 555 return doc
>>> AcceptJapanese().apply(Document("This is English document")).is_rejected
True
>>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
True
>>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
False
Inherited Members
558class DiscardRareKuten(Filter): 559 """ 560 日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます 561 ドキュメントを句点"。"で区切り, 平均文長が 562 `max_avarage_sentence_length` より長い場合は破棄します. 563 `max_avarage_sentence_length` のデフォルト値は100です. 564 このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します. 565 """ 566 567 def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None: 568 super().__init__(*args, **kwargs) 569 570 self.max_average_sentence_length = max_average_sentence_length 571 self.kuten_pat = re.compile(r"。") 572 573 def apply(self, doc: Document) -> Document: 574 """ 575 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected 576 False 577 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected 578 True 579 """ 580 kuten_lst = self.kuten_pat.findall(doc.text) 581 min_kuten_num = len(doc.text) / self.max_average_sentence_length 582 if len(kuten_lst) < min_kuten_num: 583 doc.is_rejected = True 584 return doc
日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます
ドキュメントを句点"。"で区切り, 平均文長が
max_avarage_sentence_length
より長い場合は破棄します.
max_avarage_sentence_length
のデフォルト値は100です.
このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します.
567 def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None: 568 super().__init__(*args, **kwargs) 569 570 self.max_average_sentence_length = max_average_sentence_length 571 self.kuten_pat = re.compile(r"。")
Parameters
p : float, optional Probability that this filter will be applied. Default=1
573 def apply(self, doc: Document) -> Document: 574 """ 575 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected 576 False 577 >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected 578 True 579 """ 580 kuten_lst = self.kuten_pat.findall(doc.text) 581 min_kuten_num = len(doc.text) / self.max_average_sentence_length 582 if len(kuten_lst) < min_kuten_num: 583 doc.is_rejected = True 584 return doc
>>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
False
>>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
True
Inherited Members
649class MaskPersonalInformation(Filter): 650 """ 651 ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします. 652 """ 653 654 def __init__(self, *args: Any, **kwargs: Any) -> None: 655 super().__init__(*args, **kwargs) 656 657 self.phone_pat = re.compile( 658 r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}" # noqa 659 ) 660 self.email_pat = re.compile( 661 r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)" # noqa 662 ) 663 664 def apply(self, doc: Document) -> Document: 665 """ 666 >>> MaskPersonalInformation()('06-1234-5678') 667 '06-1234-XXXX' 668 >>> MaskPersonalInformation()('075-123-4567') 669 '075-123-XXXX' 670 >>> MaskPersonalInformation()('0166-12-3456') 671 '0166-12-XXXX' 672 >>> MaskPersonalInformation()('09808-1-2345') 673 '09808-1-XXXX' 674 >>> MaskPersonalInformation()('090-1234-5678') 675 '090-1234-XXXX' 676 >>> MaskPersonalInformation()('0751234567') 677 '075123XXXX' 678 >>> MaskPersonalInformation()('08012345678') 679 '0801234XXXX' 680 >>> MaskPersonalInformation()('連絡は075-123-4567 まで') 681 '連絡は075-123-XXXX まで' 682 >>> MaskPersonalInformation()('+81-80-1234-5678') 683 '+81-80-1234-XXXX' 684 >>> MaskPersonalInformation()('+818012345678') 685 '+81801234XXXX' 686 >>> MaskPersonalInformation()('hogehoge@example.com') 687 'xxxx@yyy.com' 688 >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡') 689 '何かあれば xxxx@yyy.jp まで連絡' 690 """ 691 text = self.phone_pat.sub(r"\1XXXX", doc.text) 692 text = self.email_pat.sub(r"xxxx@yyy\1", text) 693 doc.text = text 694 return doc
ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします.
654 def __init__(self, *args: Any, **kwargs: Any) -> None: 655 super().__init__(*args, **kwargs) 656 657 self.phone_pat = re.compile( 658 r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}" # noqa 659 ) 660 self.email_pat = re.compile( 661 r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)" # noqa 662 )
Parameters
p : float, optional Probability that this filter will be applied. Default=1
664 def apply(self, doc: Document) -> Document: 665 """ 666 >>> MaskPersonalInformation()('06-1234-5678') 667 '06-1234-XXXX' 668 >>> MaskPersonalInformation()('075-123-4567') 669 '075-123-XXXX' 670 >>> MaskPersonalInformation()('0166-12-3456') 671 '0166-12-XXXX' 672 >>> MaskPersonalInformation()('09808-1-2345') 673 '09808-1-XXXX' 674 >>> MaskPersonalInformation()('090-1234-5678') 675 '090-1234-XXXX' 676 >>> MaskPersonalInformation()('0751234567') 677 '075123XXXX' 678 >>> MaskPersonalInformation()('08012345678') 679 '0801234XXXX' 680 >>> MaskPersonalInformation()('連絡は075-123-4567 まで') 681 '連絡は075-123-XXXX まで' 682 >>> MaskPersonalInformation()('+81-80-1234-5678') 683 '+81-80-1234-XXXX' 684 >>> MaskPersonalInformation()('+818012345678') 685 '+81801234XXXX' 686 >>> MaskPersonalInformation()('hogehoge@example.com') 687 'xxxx@yyy.com' 688 >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡') 689 '何かあれば xxxx@yyy.jp まで連絡' 690 """ 691 text = self.phone_pat.sub(r"\1XXXX", doc.text) 692 text = self.email_pat.sub(r"xxxx@yyy\1", text) 693 doc.text = text 694 return doc
>>> MaskPersonalInformation()('06-1234-5678')
'06-1234-XXXX'
>>> MaskPersonalInformation()('075-123-4567')
'075-123-XXXX'
>>> MaskPersonalInformation()('0166-12-3456')
'0166-12-XXXX'
>>> MaskPersonalInformation()('09808-1-2345')
'09808-1-XXXX'
>>> MaskPersonalInformation()('090-1234-5678')
'090-1234-XXXX'
>>> MaskPersonalInformation()('0751234567')
'075123XXXX'
>>> MaskPersonalInformation()('08012345678')
'0801234XXXX'
>>> MaskPersonalInformation()('連絡は075-123-4567 まで')
'連絡は075-123-XXXX まで'
>>> MaskPersonalInformation()('+81-80-1234-5678')
'+81-80-1234-XXXX'
>>> MaskPersonalInformation()('+818012345678')
'+81801234XXXX'
>>> MaskPersonalInformation()('hogehoge@example.com')
'xxxx@yyy.com'
>>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
'何かあれば xxxx@yyy.jp まで連絡'
Inherited Members
697class DiscardTooManyNouns(Filter): 698 """ 699 [!CAUTION] This filter requires `fugashi` package. Please install it 700 by `pip install 'hojichar[all]'`. 701 702 A filter that removes document with too many nouns in Japanese i.e., 703 documents such as advertisement, word salad, etc ... 704 """ 705 706 def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None: 707 """ 708 Args: 709 threshold: document whose noun ratio is higher than this value will be discarded 710 *args: 711 **kwargs: 712 """ 713 super().__init__(*args, **kwargs) 714 assert ( 715 is_loaded_extras 716 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 717 718 self.threshold = threshold 719 self.tagger = Tagger("-Owakati") 720 assert ( 721 "unidic" in self.tagger.dictionary_info[0]["filename"] 722 ), "MeCab dictionary must be unidic" 723 724 def apply(self, doc: Document) -> Document: 725 """ 726 >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected 727 False 728 >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected 729 True 730 >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected 731 False 732 """ 733 # remove "補助記号" from part-of-speech statistics 734 # because they often decrease the noun ratio, 735 # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5 736 # however, we don't want such sentence 737 pos_count = Counter( 738 w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号" 739 ) 740 try: 741 noun_ratio = pos_count["名詞"] / sum(pos_count.values()) 742 except ZeroDivisionError: 743 noun_ratio = 0.0 744 if noun_ratio >= self.threshold: 745 doc.is_rejected = True 746 return doc
[!CAUTION] This filter requires fugashi
package. Please install it
by pip install 'hojichar[all]'
.
A filter that removes document with too many nouns in Japanese i.e., documents such as advertisement, word salad, etc ...
706 def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None: 707 """ 708 Args: 709 threshold: document whose noun ratio is higher than this value will be discarded 710 *args: 711 **kwargs: 712 """ 713 super().__init__(*args, **kwargs) 714 assert ( 715 is_loaded_extras 716 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 717 718 self.threshold = threshold 719 self.tagger = Tagger("-Owakati") 720 assert ( 721 "unidic" in self.tagger.dictionary_info[0]["filename"] 722 ), "MeCab dictionary must be unidic"
Args: threshold: document whose noun ratio is higher than this value will be discarded args: *kwargs:
724 def apply(self, doc: Document) -> Document: 725 """ 726 >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected 727 False 728 >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected 729 True 730 >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected 731 False 732 """ 733 # remove "補助記号" from part-of-speech statistics 734 # because they often decrease the noun ratio, 735 # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5 736 # however, we don't want such sentence 737 pos_count = Counter( 738 w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号" 739 ) 740 try: 741 noun_ratio = pos_count["名詞"] / sum(pos_count.values()) 742 except ZeroDivisionError: 743 noun_ratio = 0.0 744 if noun_ratio >= self.threshold: 745 doc.is_rejected = True 746 return doc
>>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
False
>>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
True
>>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
False
Inherited Members
749class CharRepetitionRatioFilter(Filter): 750 """ 751 文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します. 752 名詞の連続からなるような広告テキストを取り除くのに有効です. 753 754 実装は, BigScience で採用されていた前処理を参考にしています. 755 元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453 # noqa: E501 756 757 「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが, 758 これは文書長の影響を軽減するためだとされています. 759 760 掲示板のテキストが引っかかりやすい傾向があります. 761 13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0 762 的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう 763 """ 764 765 def __init__( 766 self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any 767 ) -> None: 768 """ 769 770 Args: 771 threshold: document with character repetition ratio higher than this value will be discarded 772 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 773 *args: 774 **kwargs: 775 """ # noqa: E501 776 777 super().__init__(*args, **kwargs) 778 self.threshold = threshold 779 self.ngram_size = ngram_size 780 781 def apply(self, doc: Document) -> Document: 782 ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size) 783 if ratio >= self.threshold: 784 doc.is_rejected = True 785 return doc 786 787 @staticmethod 788 def compute_character_repetition_ratio( 789 document: str, character_repetition_length: int 790 ) -> float: 791 def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]: 792 character_ngrams: List[str] = [ 793 document[i : i + n] for i in range(len(document) - n + 1) 794 ] 795 freq_character_ngrams_dict: Dict[str, int] = {} 796 for character_ngram in character_ngrams: 797 freq_character_ngrams_dict[character_ngram] = ( 798 freq_character_ngrams_dict.get(character_ngram, 0) + 1 799 ) 800 return freq_character_ngrams_dict 801 802 freq_character_ngrams_dict = get_freq_character_ngrams( 803 document, character_repetition_length 804 ) 805 if len(freq_character_ngrams_dict) == 0: 806 return 0.0 807 freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values()) 808 freq_character_ngrams = sorted(freq_character_ngrams, reverse=True) 809 val_one = len([el for el in freq_character_ngrams if el == 1]) 810 num_rep_character_ngrams = min( 811 int(np.sqrt(len(freq_character_ngrams))), 812 len(freq_character_ngrams) - val_one, 813 ) 814 character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum( 815 freq_character_ngrams 816 ) 817 return character_repetition_ratio
文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します. 名詞の連続からなるような広告テキストを取り除くのに有効です.
実装は, BigScience で採用されていた前処理を参考にしています. 元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453 # noqa: E501
「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが, これは文書長の影響を軽減するためだとされています.
掲示板のテキストが引っかかりやすい傾向があります. 13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0 的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう
765 def __init__( 766 self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any 767 ) -> None: 768 """ 769 770 Args: 771 threshold: document with character repetition ratio higher than this value will be discarded 772 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 773 *args: 774 **kwargs: 775 """ # noqa: E501 776 777 super().__init__(*args, **kwargs) 778 self.threshold = threshold 779 self.ngram_size = ngram_size
Args: threshold: document with character repetition ratio higher than this value will be discarded ngram_size: character ngram size. Larger value will decrease the false positive of long documents args: *kwargs:
781 def apply(self, doc: Document) -> Document: 782 ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size) 783 if ratio >= self.threshold: 784 doc.is_rejected = True 785 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
787 @staticmethod 788 def compute_character_repetition_ratio( 789 document: str, character_repetition_length: int 790 ) -> float: 791 def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]: 792 character_ngrams: List[str] = [ 793 document[i : i + n] for i in range(len(document) - n + 1) 794 ] 795 freq_character_ngrams_dict: Dict[str, int] = {} 796 for character_ngram in character_ngrams: 797 freq_character_ngrams_dict[character_ngram] = ( 798 freq_character_ngrams_dict.get(character_ngram, 0) + 1 799 ) 800 return freq_character_ngrams_dict 801 802 freq_character_ngrams_dict = get_freq_character_ngrams( 803 document, character_repetition_length 804 ) 805 if len(freq_character_ngrams_dict) == 0: 806 return 0.0 807 freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values()) 808 freq_character_ngrams = sorted(freq_character_ngrams, reverse=True) 809 val_one = len([el for el in freq_character_ngrams if el == 1]) 810 num_rep_character_ngrams = min( 811 int(np.sqrt(len(freq_character_ngrams))), 812 len(freq_character_ngrams) - val_one, 813 ) 814 character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum( 815 freq_character_ngrams 816 ) 817 return character_repetition_ratio
Inherited Members
820class WordRepetitionRatioFilter(Filter): 821 """ 822 [!CAUTION] This filter requires `fugashi` package. Please install it 823 by `pip install 'hojichar[all]'`. 824 825 単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ. 826 BigScienceで採用されていた前処理を参考にしている. 827 828 名詞が連打されているような広告テキストを取り除くのに有効な様子 829 まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない 830 例: 831 "ウェブ\n本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ)\n2013/05/10(10:57) 832 ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ 833 られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる 834 なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上 835 高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら 836 経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時 837 56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ)\n2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄 838 り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入 839 るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増 840 益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、 841 電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ 842 イの回復で収益が急回" 843 """ # noqa: E501 844 845 def __init__( 846 self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any 847 ) -> None: 848 """ 849 850 Args: 851 threshold: document whose character repetition ratio is higher than this value will be discarded 852 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 853 *args: 854 **kwargs: 855 """ # noqa: E501 856 super().__init__(*args, **kwargs) 857 assert ( 858 is_loaded_extras 859 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 860 861 self.threshold = threshold 862 self.ngram_size = ngram_size 863 self.tagger = Tagger("-Owakati") 864 865 def apply(self, doc: Document) -> Document: 866 ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size) 867 if ratio >= self.threshold: 868 doc.is_rejected = True 869 return doc 870 871 def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float: 872 def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]: 873 # tokenizing given document 874 words = [w.surface for w in self.tagger(document)] 875 word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)] 876 freq_word_ngrams: Dict[str, int] = {} 877 for word_ngram in word_ngrams: 878 freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1 879 return freq_word_ngrams 880 881 freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length) 882 if len(freq_word_ngrams_dict) == 0: 883 return 0 884 freq_word_ngrams = list(freq_word_ngrams_dict.values()) 885 word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum( 886 freq_word_ngrams 887 ) 888 889 return word_repetition_ratio
[!CAUTION] This filter requires fugashi
package. Please install it
by pip install 'hojichar[all]'
.
単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ.
BigScienceで採用されていた前処理を参考にしている.
名詞が連打されているような広告テキストを取り除くのに有効な様子
まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない
例:
"ウェブ
本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ) 2013/05/10(10:57) ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上 高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら 経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時 56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ) 2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄 り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入 るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増 益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、 電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ イの回復で収益が急回"
845 def __init__( 846 self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any 847 ) -> None: 848 """ 849 850 Args: 851 threshold: document whose character repetition ratio is higher than this value will be discarded 852 ngram_size: character ngram size. Larger value will decrease the false positive of long documents 853 *args: 854 **kwargs: 855 """ # noqa: E501 856 super().__init__(*args, **kwargs) 857 assert ( 858 is_loaded_extras 859 ), "fugashi is required for this filter. Try pip install 'hojichar[all]'" 860 861 self.threshold = threshold 862 self.ngram_size = ngram_size 863 self.tagger = Tagger("-Owakati")
Args: threshold: document whose character repetition ratio is higher than this value will be discarded ngram_size: character ngram size. Larger value will decrease the false positive of long documents args: *kwargs:
865 def apply(self, doc: Document) -> Document: 866 ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size) 867 if ratio >= self.threshold: 868 doc.is_rejected = True 869 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
871 def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float: 872 def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]: 873 # tokenizing given document 874 words = [w.surface for w in self.tagger(document)] 875 word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)] 876 freq_word_ngrams: Dict[str, int] = {} 877 for word_ngram in word_ngrams: 878 freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1 879 return freq_word_ngrams 880 881 freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length) 882 if len(freq_word_ngrams_dict) == 0: 883 return 0 884 freq_word_ngrams = list(freq_word_ngrams_dict.values()) 885 word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum( 886 freq_word_ngrams 887 ) 888 889 return word_repetition_ratio
Inherited Members
892class DiscardTooManySpecialToken(Filter): 893 """ 894 [!CAUTION] This filter requires `emoji` package. Please install it 895 by `pip install 'hojichar[all]'`. 896 897 句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ 898 元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16 # noqa: E501 899 """ 900 901 def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None: 902 """ 903 904 Args: 905 threshold: document whose special token ratio is higher than this value will be discarded 906 *args: 907 **kwargs: 908 """ # noqa: E501 909 super().__init__(*args, **kwargs) 910 911 # digits are not regarded as special tokens 912 # otherwise many false positives are made, i.e., good documents discarded 913 main_special_characters = string.punctuation + string.whitespace # + string.digits 914 other_special_characters = ( 915 " ’“”–▬…✦�£•€«»°·═" 916 "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰ ‑≤≥‖" 917 "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚" 918 "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖" 919 "」﴾》�" 920 ) 921 922 en_emoji = emoji.EMOJI_DATA.keys() 923 924 special_characters_default = set(main_special_characters + other_special_characters) 925 special_characters_default.update(en_emoji) 926 self.special_characters = special_characters_default 927 928 self.threshold = threshold 929 930 def _compute_special_characters_ratio(self, text: str) -> float: 931 if len(text) == 0: 932 return 0 933 934 special_characters_ratio = len( 935 [char for char in text if char in self.special_characters] 936 ) / len(text) 937 return special_characters_ratio 938 939 def apply(self, doc: Document) -> Document: 940 special_characters_ratio = self._compute_special_characters_ratio(doc.text) 941 942 if special_characters_ratio > self.threshold: 943 doc.is_rejected = True 944 return doc
[!CAUTION] This filter requires emoji
package. Please install it
by pip install 'hojichar[all]'
.
句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ 元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16 # noqa: E501
901 def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None: 902 """ 903 904 Args: 905 threshold: document whose special token ratio is higher than this value will be discarded 906 *args: 907 **kwargs: 908 """ # noqa: E501 909 super().__init__(*args, **kwargs) 910 911 # digits are not regarded as special tokens 912 # otherwise many false positives are made, i.e., good documents discarded 913 main_special_characters = string.punctuation + string.whitespace # + string.digits 914 other_special_characters = ( 915 " ’“”–▬…✦�£•€«»°·═" 916 "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰ ‑≤≥‖" 917 "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚" 918 "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖" 919 "」﴾》�" 920 ) 921 922 en_emoji = emoji.EMOJI_DATA.keys() 923 924 special_characters_default = set(main_special_characters + other_special_characters) 925 special_characters_default.update(en_emoji) 926 self.special_characters = special_characters_default 927 928 self.threshold = threshold
Args: threshold: document whose special token ratio is higher than this value will be discarded args: *kwargs:
939 def apply(self, doc: Document) -> Document: 940 special_characters_ratio = self._compute_special_characters_ratio(doc.text) 941 942 if special_characters_ratio > self.threshold: 943 doc.is_rejected = True 944 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
947class SingleCharacterRepetitionFilter(Filter): 948 """ 949 単一文字が大量に繰り返されているような文書を取り除くためのフィルタ 950 そのような文書はノイズである可能性が高いため 951 参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい 952 https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset # noqa: E501 953 """ 954 955 def __init__( 956 self, 957 threshold: int = 200, 958 *args: Any, 959 **kwargs: Any, 960 ) -> None: 961 """ 962 Args: 963 threshold: The document is removed if character is repeated for this value or more 964 *args: 965 **kwargs: 966 """ 967 super().__init__(*args, **kwargs) 968 self.threshold = threshold 969 970 def _is_repeat_contained(self, text: str) -> bool: 971 groups = groupby(text) 972 is_repeat_contained = any(sum(1 for _ in group) >= self.threshold for _, group in groups) 973 return is_repeat_contained 974 975 def apply(self, doc: Document) -> Document: 976 if self._is_repeat_contained(doc.text): 977 doc.is_rejected = True 978 return doc
単一文字が大量に繰り返されているような文書を取り除くためのフィルタ そのような文書はノイズである可能性が高いため 参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset # noqa: E501
955 def __init__( 956 self, 957 threshold: int = 200, 958 *args: Any, 959 **kwargs: Any, 960 ) -> None: 961 """ 962 Args: 963 threshold: The document is removed if character is repeated for this value or more 964 *args: 965 **kwargs: 966 """ 967 super().__init__(*args, **kwargs) 968 self.threshold = threshold
Args: threshold: The document is removed if character is repeated for this value or more args: *kwargs:
975 def apply(self, doc: Document) -> Document: 976 if self._is_repeat_contained(doc.text): 977 doc.is_rejected = True 978 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
981class DiscardTooManyEndingEllipsis(Filter): 982 """ 983 ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです. 984 ellipsisとしては ... と … を用いている 985 同様のフィルタが RedPajama v2で用いられています. 986 987 例として, 以下のような文書を検知します. 988 ``` 989 ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付... 990 バツイチアラフォー 婚活ち女性の特徴と子持な付... 991 ``` 992 993 デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、 994 precisionを重視した設定です. 995 """ 996 997 def __init__( 998 self, 999 threshold: float = 0.7, 1000 *args: Any, 1001 **kwargs: Any, 1002 ) -> None: 1003 """ 1004 Args: 1005 threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value 1006 *args: 1007 **kwargs: 1008 """ # noqa: E501 1009 super().__init__(*args, **kwargs) 1010 self.threshold = threshold 1011 self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n") # matches ...\n and …\n 1012 1013 def apply(self, doc: Document) -> Document: 1014 ellipsis_count = len(self.ellipsis_pattern.findall(doc.text)) 1015 newline_count = max(doc.text.count("\n"), 1) # avoid zero division 1016 ellipsis_ratio = ellipsis_count / newline_count 1017 1018 if ellipsis_ratio > self.threshold: 1019 doc.is_rejected = True 1020 return doc
ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです. ellipsisとしては ... と … を用いている 同様のフィルタが RedPajama v2で用いられています.
例として, 以下のような文書を検知します.
ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付...
バツイチアラフォー 婚活ち女性の特徴と子持な付...
デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、 precisionを重視した設定です.
997 def __init__( 998 self, 999 threshold: float = 0.7, 1000 *args: Any, 1001 **kwargs: Any, 1002 ) -> None: 1003 """ 1004 Args: 1005 threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value 1006 *args: 1007 **kwargs: 1008 """ # noqa: E501 1009 super().__init__(*args, **kwargs) 1010 self.threshold = threshold 1011 self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n") # matches ...\n and …\n
Args: threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value args: *kwargs:
1013 def apply(self, doc: Document) -> Document: 1014 ellipsis_count = len(self.ellipsis_pattern.findall(doc.text)) 1015 newline_count = max(doc.text.count("\n"), 1) # avoid zero division 1016 ellipsis_ratio = ellipsis_count / newline_count 1017 1018 if ellipsis_ratio > self.threshold: 1019 doc.is_rejected = True 1020 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document
Inherited Members
1023class DiscardTooShortLines(Filter): 1024 """ 1025 短い行を大量に含む文書を捨てるためのフィルタです. 1026 1027 メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です. 1028 """ 1029 1030 def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None: 1031 """ 1032 Args: 1033 threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value. 1034 *args: 1035 **kwargs: 1036 """ # noqa: E501 1037 super().__init__(*args, **kwargs) 1038 self.threshold = threshold 1039 # この値は適当に決め打ち 1040 self.minimum_line_length = 10 1041 1042 def apply(self, doc: Document) -> Document: 1043 lines = [len(x) for x in doc.text.split("\n")] 1044 short_lines = [x for x in lines if x <= self.minimum_line_length] 1045 if (len(short_lines) / len(lines)) > self.threshold: 1046 doc.is_rejected = True 1047 return doc
短い行を大量に含む文書を捨てるためのフィルタです.
メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です.
1030 def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None: 1031 """ 1032 Args: 1033 threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value. 1034 *args: 1035 **kwargs: 1036 """ # noqa: E501 1037 super().__init__(*args, **kwargs) 1038 self.threshold = threshold 1039 # この値は適当に決め打ち 1040 self.minimum_line_length = 10
Args: threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value. args: *kwargs:
1042 def apply(self, doc: Document) -> Document: 1043 lines = [len(x) for x in doc.text.split("\n")] 1044 short_lines = [x for x in lines if x <= self.minimum_line_length] 1045 if (len(short_lines) / len(lines)) > self.threshold: 1046 doc.is_rejected = True 1047 return doc
Definition of filter behavior.
In this method, the filter will modify document.text
, or
set document.is_rejected = True
to discard the document.
Do not define a filter that changes both document.text
and document.token
Parameters
document : Document Input document
Returns
Document Processed Document