🪚

LightBGMで日本語Webデータから不要な行を除去する

2024/06/06に公開

要約

  • LightGBMを用いて、日本語文章から不要な行を除去するモデルを学習した
  • 特徴量として、品詞情報、文字数、記号数、数字の割合、日付やURLの出現回数などを使用
  • フィルタリング前後のデータでmistralアーキテクチャのモデル(370Mパラメータ)を1から事前学習し、lossの改善を確認

目的

LLMを事前学習する際、データの品質が学習効率や最終的なモデルの性能に大きな影響を与えると言われています。
そこで、LightGBMを用いた不要行判定モデルを学習することで、事前学習コーパスの品質向上を目指します。

実施内容

アノテーション

llm-jp-corpus-v2のja_ccの文章から12,000行の要不要をアノテーションしました。
うろ覚えですが、4-6時間くらいかかったと思います。

不要行除去モデルの学習

ABEJAさんの記事を参考に、LightGBMを用いて不要行判定モデルを学習しました。
https://tech-blog.abeja.asia/entry/abeja-nedo-project-part2-202405#MLベースのフィルタリング

特徴量として以下を使用しています。

# 特徴量の作成
def create_features(df):
    # 品詞のカウント・割合
    def count_pos(text, pos):
        node = tagger.parseToNode(text)
        count = 0
        while node:
            if node.feature.split(",")[0] == pos:
                count += 1
            node = node.next
        return count

    def count_words(text):
        node = tagger.parseToNode(text)
        count = 0
        while node:
            if node.feature.split(",")[0] != "BOS/EOS":
                count += 1
            node = node.next
        return count

    df["noun_count"] = df["sentence"].apply(lambda x: count_pos(x, "名詞"))
    df["verb_count"] = df["sentence"].apply(lambda x: count_pos(x, "動詞"))
    df["adj_count"] = df["sentence"].apply(lambda x: count_pos(x, "形容詞"))
    df["word_count"] = df["sentence"].apply(count_words)
    df["noun_ratio"] = df["noun_count"] / df["word_count"]
    df["verb_ratio"] = df["verb_count"] / df["word_count"]
    df["adj_ratio"] = df["adj_count"] / df["word_count"]

    # 文字、句読点、記号、省略記号(…, ...)、数字の数
    df["char_count"] = df["sentence"].str.len()
    df["punct_count"] = df["sentence"].str.count("[。、!?]")
    df["symbol_count"] = df["sentence"].str.count("[^a-zA-Zぁ-んァ-ン一-龥0-9]")
    df["ellipsis_count"] = df["sentence"].str.count("…|\\.\\.\\.")
    df["digit_count"] = df["sentence"].str.count("\d")

    # ひらがな・英語・数字の割合
    df["hiragana_ratio"] = df["sentence"].str.count("[ぁ-ん]") / df["char_count"]
    df["english_ratio"] = df["sentence"].str.count("[a-zA-Z]") / df["char_count"]
    df["digit_ratio"] = df["sentence"].str.count("[0-9]") / df["char_count"]

    # 日付関係の文字列、URL文字列、不要キーワードについてのカウント
    df["date_count"] = df["sentence"].str.count("\d{4}[/\-年]\d{1,2}[/\-月]?\d{0,2}[日]?")
    df["url_count"] = df["sentence"].str.count("https?://[\w/:%#\$&\?\(\)~\.=\+\-]+")
    df["keyword_count"] = df["sentence"].str.count("広告|アーカイブ|関連記事|スポンサーリンク")

    # 重要度の高い特徴量について、前後1行の情報(shift特徴量)と文章全体での集約特徴量(平均、最大)を追加
    important_features = ["noun_ratio", "verb_ratio", "adj_ratio", "digit_ratio", "hiragana_ratio", "english_ratio"]

    for feature in important_features:
        df[f"{feature}_shift_-1"] = df.groupby("text_id")[feature].shift(-1)
        df[f"{feature}_shift_1"] = df.groupby("text_id")[feature].shift(1)

        # 前後5行の情報を平均と最大で集約
        df[f"{feature}_prev_5_mean"] = df.groupby("text_id")[feature].rolling(window=5, min_periods=1).mean().reset_index(drop=True)
        df[f"{feature}_prev_5_max"] = df.groupby("text_id")[feature].rolling(window=5, min_periods=1).max().reset_index(drop=True)
        df[f"{feature}_next_5_mean"] = df.groupby("text_id")[feature].rolling(window=5, min_periods=1).mean().shift(-5).reset_index(drop=True)
        df[f"{feature}_next_5_max"] = df.groupby("text_id")[feature].rolling(window=5, min_periods=1).max().shift(-5).reset_index(drop=True)

        df[f"{feature}_mean"] = df.groupby("text_id")[feature].transform("mean")
        df[f"{feature}_max"] = df.groupby("text_id")[feature].transform("max")

    return df

# 特徴量
features = [col for col in df.columns if col not in ["text_id", "sentence", "check", "noun_count", "verb_count", "adj_count"]]

結果

不要行判定モデルの性能

学習したモデルはテストデータに対して以下のような性能になりました。

  • Accuracy: 0.8259
  • Precision: 0.8441
  • Recall: 0.8935
  • F1 Score: 0.8681

実際の判定結果を見てみると、ヘッダーやフッターのような部分はスコアが低くなり、本文は高めの値が出ています。

スコア 判定結果
正職員 0.024 0
車通勤可 0.060 0
茨城県守谷市に位置する3000坪にもおよぶ広大な敷地を有した病院です。 0.868 1
「患者様に安心していただくことが私たちの使命です」を病院の理念とし、 0.957 1
MRI、CT等の高度な医療設備を揃えて、地域に密着した医療を展開されています。 0.966 1
賞与3.8ヶ月分実績と、頑張りを評価していただけます。 0.951 1
ご興味のある方は、お気軽にお問い合わせください。 0.734 1
求人概要 0.139 0
法人概要 0.019 0

フィルタリング

文章長など基本的な要素でフィルタリングをかけて残った文章に対して、

  • 全行の予測スコアの平均が0.5未満
  • 全行の予測スコアの中央値が0.5未満
    のいずれかを満たす文章は除去、
    また、残った文章からも、予測スコアが0.22未満の行を削除しました。

フィルタリング前のデータと比較して、日本語CCのデータ量は1/4程度に削減された。

事前学習での比較

フィルタリング前後のデータを用いて、mistralアーキテクチャのモデル(パラメータ数370M)を1から事前学習して、lossを比較しました。
事前学習は以下のコードを参考にさせていただきました。
https://zenn.dev/selllous/articles/transformers_pretrain_to_ft

事前学習にはllm-jp-corpusから

  • ja_cc
  • ja_wiki
  • en_wiki

の一部を使用しています。

学習データと検証データ(日本語wikiデータ)に対してのlossは以下のようになりました。
400steps程度で打ち切っている方がフィルタリング後のデータで学習した場合です。

フィルタリング前後で比較すると、約半分のsteps数で同程度のlossまで下がっていました。
ただ、400stepsの段階では1BT程度しか学習が進んでいないため、日本語を話すことはできていませんでした。

フィルタリングの効果を比較する上での反省点として、行単位のフィルタリング以外にも文章長やn-gramでのフィルタリングを行ってしまったため、純粋な行単位のフィルタリングに効果があったのかわからないのが現状です。

まとめ

LightGBMを用いて不要行判定モデルを学習し、日本語コーパスの品質向上を図りました。
アノテーションが大変でしたが、やる価値がありそうで良かったです。

TODO

  • 余計なフィルタリングはせず、行フィルターのみでの検証を行う
  • まだヘッダー・フッターなどが残っているケースがあったため、特徴量やハイパラを調整する
  • keyword_countをもう少し凝る

Discussion