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

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

ちなみに、下記手順で抽出されたコーパス自体は
[LLM\-jp\-13B v2\.0](https://llm-jp.nii.ac.jp/blog/2024/04/30/v2.0-release.html)　の成果物の一部として、以下で公開されています。
[datasets / llm\-jp\-corpus\-v2 · GitLab](https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v2)

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

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

# 前準備
## 1.Spark環境作る
なんでもいいので、Spark環境作ります。
EKSでもオンプレでもお好みで。

## 2.uzushioビルドする

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

``` Dockerfile
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本家では別の辞書を使ってるっぽいですが、手軽に入手できるやつ
``` shell
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](http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/)　から最新のやつを。
``` shell
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を作る
必須ではないですが、作っておいた方が楽だと思います。
パスとかは適当に書き換え・読み替えてください

``` Dockerfile
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](https://commoncrawl.org/overview)から適当なのを取ってきます。
これ自体は、単なるパスのリストです。
8万行くらいあるので気長に処理することになります。

頭に`s3://commoncrawl/` を付けると、s3のパスになります。
Sparkで処理するなら `s3a://commoncrawl/` にしておくのが安牌。
例
``` shell
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](https://qiita.com/eiennohito/items/35c640ced80372906c44)

``` shell
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`

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


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

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

``` shell
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](https://qiita.com/eiennohito/items/384dc51ce3e38c5e2aa3)

ここは秘伝のタレ的な感じなので、とりあえず最初は、公式のパイプラインをそのまま使って、適宜調整していけばよいかと。
```{"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](https://github.com/WorksApplications/uzushio/blob/main/scripts/pipeline_03a.conf)

``` conf
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_PARTITIONS`、`NUM_PARTITIONS_PROPAGATION`はハッシュ計算の時のを流用します。
`cache`にはハッシュ計算の`output`をセットします。

`Pkenlm` `Psudachi`は`kenlm`系のフィルタを入れた時だけ必要。

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

``` shell
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/c09769dc-6486-05b1-bb88-d7073569b872.png)


# 参照
[Uzushio: a preprocessing tool for LLM pretraining corpora \#Scala \- Qiita](https://qiita.com/eiennohito/items/0774662332778c27116f)
[WorksApplications/uzushio](https://github.com/WorksApplications/uzushio)