日本語の事前学習データセット(OSCAR,mc4)を機械学習でクリーニングしてみる練習

はじめに

日本語の事前学習データセットを最近は触っています。
Common Crawlから直接構築することも検討中ですが、まずは既存のデータセットをクリーニングしてみるところから始めてみます。

(ルールベースで真面目に清掃するスクリプトも存在します)


2/21追記 いくらか関連するコードをgithubにuploadしました。


データセットのダウンロードと内訳チェック

huggingfaceのdatasetsクラスから、streamingで落とせます。非常に便利です。

from datasets import load_dataset


# ignore_verifications=Trueをつけないとエラーとなる
oscar_dataset = load_dataset('oscar', 'unshuffled_original_ja', 
                       split='train', 
                       ignore_verifications=True,
                       streaming=True)
#mc4の場合はこちら
mc4_dataset = load_dataset('mc4', 'ja',split='train', streaming=True)
dataset=oscar_dataset

例えばはじめの1万件を読み込む場合、適当にiterationを回します。

loc_records = []
cnt=0
for record in dataset:
    loc_records.append(record)
    cnt+=1
    if cnt>10**4:
        break

はじめの10件の内訳は以下の通り。

  • 販促サイト

  • 販促サイト

  • ニュース

  • 販促サイト

  • 販促サイト

  • ゲーム関連の一般人の記事?

  • 何かを語る記事

  • ポルノ

  • 販促サイト

  • 音楽サイト(?)

10件中、大規模言語モデルに学習させたいと思うテキストは4件だけでした。個人的に、学習データは質が大切だと思っていますので、不要なテキストを除去するスクリプトを書いていきます。

テキストのアノテーション

各テキストのはじめの100文字の情報をもとに、手作業でアノテーション(good/bad)していきます。

以下のサイトを用い、適当に100件ほど、アノテーションしました。

削除基準

  • 明らかに販売目的のサイト

  • 短すぎるサイト

  • 単語の羅列が中心のサイト

  • 公序良俗に反するサイト


機械学習による分類

logistic regressionで分けていきます。
補助関数として、以下のファイルを用います。

rom sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from src.LogisticClassifier import prepare_classifier

pipeline = prepare_classifier()

# データセットの分割
X_train, X_test, y_train, y_test = train_test_split(annotated_texts, annotated_labels, test_size=0.2, random_state=42)


# モデルの訓練
pipeline.fit(X_train, y_train)

# モデルの評価
predictions = pipeline.predict(X_test)
print(classification_report(y_test, predictions))


追加のアノテーションデータの生成

good/badの予測値が0.5付近のものを優先的にアノテーションしていきます。

import pandas as pd
import random
from tqdm import tqdm

#新たな問題の生成
n_problems=500
start_num=random.randint(1000, 10**4)
cut_threshold=0.3   #0-0.5 値が大きいほど、自信のない問題を選ぶ
problem_list=[]
cnt=-1


dataset=mc4_dataset
dataset_type="mc4"

pipeline.fit(annotated_texts, annotated_labels)
for record in tqdm(dataset):
    cnt+=1
    if cnt<start_num:
        continue
    text = record["text"]
    if dataset_type=="mc4":
        record["id"]=f"mc4_{cnt}"
    if text not in annotated_texts:
        p_text = prepare_problem_text(record, with_id=True)
        problem_list.append(p_text)
    if len(problem_list)>n_problems:
        break


unlabeled_problem_list=problem_list
annotations = pipeline.predict(unlabeled_problem_list)
predicted_probabilities = pipeline.predict_proba(unlabeled_problem_list)


df = pd.DataFrame({ "label": annotations, "probability": predicted_probabilities[:, 1],
"text": unlabeled_problem_list,
                   
                   })
df=df.sort_values(by="probability", ascending=False)
df=df[df["probability"]>cut_threshold]
df=df[df["probability"]<1-cut_threshold]

question_texts=df["text"].tolist()
with open(f"annot/pages/questions.text", "w") as f:
    f.write("\n".join(question_texts))

annot/pages/questions.textに、追加の質問が生成されるので、こちらもアノテーションします。
数百件ほどアノテーションすると、安定してきます。

データのフィルタリング

学習したモデルでフィルタリングします。
せっかくなので、0-1でスコア(確信度)を出し、0.5, 0.6, 0.7, …に分けて保存することにします。ひょっとすると、品質でテキストを分けられるかもしれないので。

import pandas as pd
import random
from tqdm import tqdm
import json

#フィルタリングしたデータセットの生成
n_problems=2000
problem_list=[]
mini_problem_list=[]
cnt=0
minibatch=1000
file_split_num=10**6

save_path=f"annot/pages/extracted_mc4.txt"
base_path=f"output/{dataset_type}/"


dataset=mc4_dataset
#dataset=oscar_dataset

for record in tqdm(dataset):
    text = record["text"]
    p_text = prepare_problem_text(record, with_id=False,)
    mini_problem_list.append((cnt,p_text,text))
    cnt+=1
    if cnt>n_problems:
        break
    if cnt%minibatch==5:    
        rids,p_texts,texts=zip(*mini_problem_list)
         #annotations = pipeline.predict(p_texts)
        prob=pipeline.predict_proba(p_texts)[:,0]


        for i in range(len(prob)):
            int_prob=int(prob[i]*10)
            prefix=int(cnt/file_split_num)
            #ちょっとカットオフを厳し目にする
            if prob[i]>0.55:
                #せっかくなので、確信度で分けて保存する
                with open(f"{base_path}/{prefix}_class_{int_prob}.txt", "a") as f:
                    j_line=json.dumps({"id":rids[i], "text":texts[i]},ensure_ascii=False)
                    f.write(j_line+"\n")
            
        mini_problem_list=[]

結果

oscarは2000→500件
mc4は2000→330件
までフィルタリングされました。

完璧なフィルタリング精度では有りませんが、わりといい感じのテキストを抽出できたと思います。

フィルタ前

青は公序良俗に反するサイト

フィルタ後


mc4の処理速度が500it/sec程度でした。87,337,884件のデータがあるとのことなので、ざっくり2日ほど放置すれば、フィルタリングが終わる計算になります。ローカルマシンでも行える処理なので、十分に許容できる速度だと思います。

まとめ

このフィルタ効率でいくと
mc4: 830GB → 400GB
Oscar:110GB→30GB
くらいまで減らせそうです。


この記事が気に入ったらサポートをしてみませんか?