2
1

uzushio使ってwebコンテンツから綺麗な日本語コーパスを抽出する手順

Last updated at Posted at 2024-05-13

本家のドキュメント整備が後回しにされているようなので、メモ&共有。

ちなみに、下記手順で抽出されたコーパス自体は
LLM-jp-13B v2.0 の成果物の一部として、以下で公開されています。
datasets / llm-jp-corpus-v2 · GitLab

uzushioとは

WorksApplications/uzushio
CommonCrawlのデータから綺麗な日本語コーパスを抽出するためのライブラリです。
控えめに言って神なのですが、ドキュメントが足らず、結局本家のソースを解読したりする羽目になったので、未来の自分と、同じようなことで迷子になった方のために書き残します。

なお、日本語のクレンジング部分のみ利用したい場合、中間データを成形してやれば手持ちのデータにも適用できます。

前準備

1.Spark環境作る

なんでもいいので、Spark環境作ります。
EKSでもオンプレでもお好みで。

2.uzushioビルドする

正直ここが一番の鬼門、、、
なのでDockerfileをご用意しました!(ローカル環境絶対汚したくないマン)
kenlm系のフィルターを使わないなら、kenlm要らないのですが、ビルド的には誤差なのでとりあえず入れておいて損はないかと。

FROM maven:3.8.6-openjdk-11-slim

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential libboost-all-dev zlib1g-dev libbz2-dev liblzma-dev \
    cmake \
    g++  \
    git \
    swig \
    wget \
    unzip \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# make dest dir
WORKDIR /work/dest
# build kenlm
WORKDIR /work/build_kenlm
RUN wget https://kheafield.com/code/kenlm.tar.gz -O kenlm.tar.gz && tar -xvzf kenlm.tar.gz  
RUN mkdir kenlm/build \
    && cd kenlm/build \
    && cmake .. \
    && make -j2 \
    && make install

# build kenlm-java
ENV CPATH=$JAVA_HOME/include:$JAVA_HOME/include/:$JAVA_HOME/include/linux:/work/build_kenlm/kenlm
RUN git clone https://github.com/eiennohito/kenlm-java 
RUN cd kenlm-java \
    && ./build.sh \
    && cp libkenlm-jni.so $JAVA_HOME/lib/ \
    && mvn package -Dmaven.javadoc.skip=true \
    && cp target/*.jar /work/dest/ \
    && cp libkenlm-jni.so /work/dest/

# build uzushio
WORKDIR /work/build_uzushio
ARG SBT_URL="https://github.com/sbt/sbt/releases/download/v1.9.8/sbt-1.9.8.zip"
RUN wget ${SBT_URL} -O sbt.zip && unzip sbt.zip && cp sbt/bin/* /usr/local/bin/

RUN git clone https://github.com/WorksApplications/uzushio
RUN cd uzushio \
  && sbt assembly \
  && cp core/target/scala-2.12/*.jar /work/dest/

# WORKDIR /work/dest/
CMD ["tail", "-f", "/dev/null"]

やってること

  • kenlmをビルドする
  • uzushio用のkenlmラッパーをビルドする
  • uzushioをビルドする
  • 必要なバイナリをdestフォルダに全部放り込む

あとは、ビルド結果を抽出すればOKです

build_uzushio.sh
dest_to=/work/uzushio/jars/

docker build -t uzushio/uzushio-build .
run_id=`docker create uzushio/uzushio-build`
docker cp $run_id:/work/dest/ $dest_to
docker rm $run_id

3.kenlmのモデル、sudachiの辞書をゲットする

  • kenlm
    uzushio本家では別の辞書を使ってるっぽいですが、手軽に入手できるやつ
mkdir -p kenlm
wget -c  -P kenlm http://dl.fbaipublicfiles.com/cc_net/lm/ja.arpa.bin
wget -c  -P kenlm http://dl.fbaipublicfiles.com/cc_net/lm/ja.sp.model
  • sudachi
    こちら SudachiDict から最新のやつを。
wget -c http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/sudachi-dictionary-latest-full.zip
unzip sudachi-dictionary-latest-full.zip

3.uzushio入りのSpark Imageを作る

必須ではないですが、作っておいた方が楽だと思います。
パスとかは適当に書き換え・読み替えてください

FROM spark:3.4.1-scala2.12-java11-python3-ubuntu

USER spark

# jarsをコピー
ENV SPARK_EXTRA_JARS_DIR=/usr/local/spark/jars/
COPY --chown=spark /work/uzushio/jars/* $SPARK_EXTRA_JARS_DIR
COPY --chown=spark /work/uzushio/jars/* "$SPARK_HOME/jars/"
# 辞書をコピー
COPY kenlm/lm_sp/* /usr/local/spark/uzushio/kenlm/
COPY sudachi/* /usr/local/spark/uzushio/sudachi/

4.CCのアーカイブを取ってくる

Common Crawl - Overviewから適当なのを取ってきます。
これ自体は、単なるパスのリストです。
8万行くらいあるので気長に処理することになります。

頭にs3://commoncrawl/ を付けると、s3のパスになります。
Sparkで処理するなら s3a://commoncrawl/ にしておくのが安牌。

wget https://data.commoncrawl.org/crawl-data/CC-MAIN-2024-18/warc.paths.gz
gzip -d warc.paths.gz

テキスト抽出&クレンジング

ようやく本番!

1.日本語テキスト抽出

uzushioが日本語っぽいのだけ抽出してくれます。

inputにCCのパスをカンマ区切りで突っ込みます。
outputにparquetが保存されます。
outputはs3aとかでも大丈夫です。
ディレクトリに上書きされるので、分割して取得する場合は、適宜フォルダを切ります。

手元のデータを処理したい場合には、この出力に合わせてデータを成形します。
(後述)

この時点では、HTMLパスと文章が保持されます。
公式記事:Uzushio: Text Extraction #Scala - Qiita

export CC_PATH=s3a://commoncrawl/crawl-data/CC-MAIN-2024-18/segments/1712296815919.75/warc/CC-MAIN-20240412101354-20240412131354-00000.warc.gz,s3a://commoncrawl/crawl-data/CC-MAIN-2024-18/segments/1712296815919.75/warc/CC-MAIN-20240412101354-20240412131354-00001.warc.gz
export OUTPUT=/work/cc/extract

spark-submit \
    --class=com.worksap.nlp.uzushio.main.ExtractTextFromWarc \
    --master='local[*]' \
    /usr/local/spark/jars//uzushio-assembly-0.1-SNAPSHOT.jar \
    --input=$CC_PATH \
    --output=$OUTPUT \
    --language=ja

1.5 手持ちのデータをuzushio形式に加工する

CCじゃなくて自前で集めたデータを綺麗にしたいんだよなー、ってとき。
以下の項目を作って・集めておけばOK。
あるいは、自分で集めたデータを、一旦CCと同等のWarcにするのが推奨されていますが、面倒なので割愛。

  • docId ユニークなUUIDとかでOK
  • text クレンジング対象の文章
  • charset
  • date
  • url

dateurlは内部的に同じURLのコンテンツを切り捨てるのに使ってる?
まあ、それらしい値を入れておけばOK。

2.ハッシュ計算

uzushioでは重複排除の高速化のために、文書のハッシュ値を使っているので、これを事前に計算します。
NUM_PARTITIONSNUM_PARTITIONS_PROPAGATIONはコーパスのサイズに合わせる必要があります。(多分)
公式の値を見ると、概ねNUM_PARTITIONS_PROPAGATION=NUM_PARTITIONS*4 あたりでいいっぽい。
NUM_PARTITIONSの求め方はヨクワカラナイ。。。

本家の設定では、CCの1アーカイブにつき、NUM_PARTITIONS=500とかが使われてます。

export INPUT=/work/cc/extract
export HASH=/work/cc/hash
export NUM_PARTITIONS=50
export NUM_PARTITIONS_PROPAGATION=200

spark-submit \
    --class com.worksap.nlp.uzushio.main.DeduplicateParagraphs \
    --master='local[*]' \
    --conf spark.sql.shuffle.partitions=${NUM_PARTITIONS_PROPAGATION} \
    --conf spark.sql.parquet.columnarReaderBatchSize=512 \
    /usr/local/spark/jars//uzushio-assembly-0.1-SNAPSHOT.jar \
    --input="$INPUT" \
    --output="$HASH" \
    --execution=reprHashes,stats,saveStats \
    --propagate-partitions=$NUM_PARTITIONS_PROPAGATION \
    --partitions=$NUM_PARTITIONS \
    --intermediate

3.フィルタパイプライン

ここからがuzushioの本領発揮です。

  • 長さが短いコンテンツの廃棄
  • 平仮名が多すぎる、リンクが多すぎる(ECショップ系)
  • 繰り返し文章の除外(ブログのメニューとか、サイトの定型文とか)
  • 重複したドキュメントの除去、重複した文章の段落単位での除去
  • 日本語として不自然な文章の除去
  • 不適切ワードの除去

これらをパイプライン形式で一挙に処理してくれます。
詳しくは、公式のドキュメント参照。
Uzushio: Filters #Scala - Qiita

ここは秘伝のタレ的な感じなので、とりあえず最初は、公式のパイプラインをそのまま使って、適宜調整していけばよいかと。
{"class": "MarkdownizeHeading"}, が入ってると、HTMLの構造をマークダウンに置き換えてくれるのでよきです。

平仮名率・リンク率・圧縮率とかはwebクローリングコーパス前提の処理なので、
手元の文章(へんなものが入ってない)を処理するだけなら
{"class": "DeduplicateDocumentsPercentile", "expected": 5, "percentile": 0.05},
だけでも結構いい感じの仕事をしてくれます。

あるいは日本語として自然な文章だけを抜くのに
{"class": "KenLMDocAvgPerplexity", "sudachi": ${sudachi}, "kenlm": ${kenlm}, outliers: 0.1, high: 1e6, low: 5},
トカ?

uzushio/scripts/pipeline_03a.conf at main · WorksApplications/uzushio

filters: [
    {"class": "DocLength", "low": 50},
    {"class": "DeduplicateDocumentsPercentile", "expected": 5, "percentile": 0.05},
    {"class": "HiraganaRatio", "low": 0.1, "high": 2.0},
    {"class": "HiraganaRatio", "low": 0.15, "high": 2.0},
    {"class": "LinkCharRatio", "low": 0, "high": 0.8},
    {"class": "LinkCharRatio", "low": 0, "high": 0.4},
    {"class": "MergeListTag"},
    {"class": "MarkdownizeHeading"},
    {"class": "NoContentDOM"},
    {"class": "LargeFreqParagraphs", "count": 3, "freq": 1000},
    {"class": "LargeFreqParagraphs", "count": 3, "freq": 100},
    {"class": "KenLMParagraphPerplexity", "sudachi": ${sudachi}, "kenlm": ${kenlm}, outliers: 0.1, "count": 3, "threshold": 1e6},
    {"class": "KenLMParagraphPerplexity", "sudachi": ${sudachi}, "kenlm": ${kenlm}, outliers: 0.1, "count": 2, "threshold": 5e6},
    {"class": "CompressionRate", "low": 0.25, "high": 5.0},
    {"class": "CompressionRate", "low": 0.40, "high": 0.75},
    {"class": "CompressionRate", "low": 0.50, "high": 0.75},
    {"class": "KenLMDocAvgPerplexity", "sudachi": ${sudachi}, "kenlm": ${kenlm}, outliers: 0.1, high: 1e6, low: 5},
    {"class": "KenLMDocAvgPerplexity", "sudachi": ${sudachi}, "kenlm": ${kenlm}, outliers: 0.1, high: 5e5, low: 7},
    {"class": "WordTypes", "threshold": 9, "kind": "uniq", "list": "hojichar/adult_keywords_ja.txt"},
    {"class": "WordTypes", "threshold": 6, "kind": "uniq", "list": "hojichar/adult_keywords_ja.txt"},
    {"class": "WordTypes", "threshold": 9, "kind": "uniq", "list": "hojichar/discriminations_keywords_ja.txt"},
    {"class": "DocLength", "low": 200},
    {"class": "DeduplicateDocumentsPercentile", "expected": 2.5, "percentile": 0.05},
    {"class": "DeduplicateDocumentsPercentile", "expected": 1.5, "percentile": 0.1},
]

NUM_PARTITIONSNUM_PARTITIONS_PROPAGATIONはハッシュ計算の時のを流用します。
cacheにはハッシュ計算のoutputをセットします。

Pkenlm Psudachikenlm系のフィルタを入れた時だけ必要。

なお、処理途中のデータを何回か内部で保存してるので、EKSでSpotインスタンスを使っていると永遠に処理が終わりません。👼
(いなくなったSpotインスタンスと一緒にデータも消え去る)
OnDemand でやりましょう。
あとメモリとディスクは多めに用意しておきましょう。

export INPUT=/work/cc/extract
export HASH=/work/cc/hash
export OUTPUT=/work/cc/clean
export NUM_PARTITIONS=50
export NUM_PARTITIONS_PROPAGATION=200

spark-submit \
    --class com.worksap.nlp.uzushio.main.DeduplicateParagraphs \
    --master='local[*]' \
    --conf spark.local.dir=/opt/local/spark/work \
    --conf spark.sql.shuffle.partitions=$NUM_PARTITIONS_PROPAGATION \
    --conf spark.sql.parquet.columnarReaderBatchSize=256 \
    /usr/local/spark/jars//uzushio-assembly-0.1-SNAPSHOT.jar \
    --input=$INPUT \
    --cache=$HASH \
    --output=$OUTPUT \
    --propagate-partitions=$NUM_PARTITIONS_PROPAGATION \
    --filters=$SCRIPT_DIR/pipeline_03a.conf \
    --partitions=$NUM_PARTITIONS \
    --execution=filter-debug \
    -Pkenlm=/opt/spark/uzushio_filter/kenlm/ja.arpa.bin \
    -Psudachi=/opt/spark/uzushio_filter/sudachi/system_full.dic \
    --text-only

出力をサクッと見たいときは
--format=jsonで実行すれば、jsonで保存されるはず。
あとなんかフィルターの各ステージを保存してくれるパラメータがあるはずだが既に思い出せない。(ぉ)

こんな感じでフィルタ別に出力されます
最後のやつだけ出すオプションがあったはずだが思い出せない~

image.png

参照

Uzushio: a preprocessing tool for LLM pretraining corpora #Scala - Qiita
WorksApplications/uzushio

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1