本家のドキュメント整備が後回しにされているようなので、メモ&共有。
ちなみに、下記手順で抽出されたコーパス自体は
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です
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
date
とurl
は内部的に同じURLのコンテンツを切り捨てるのに使ってる?
まあ、それらしい値を入れておけばOK。
2.ハッシュ計算
uzushio
では重複排除の高速化のために、文書のハッシュ値を使っているので、これを事前に計算します。
NUM_PARTITIONS
、NUM_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_PARTITIONS
、NUM_PARTITIONS_PROPAGATION
はハッシュ計算の時のを流用します。
cache
にはハッシュ計算のoutput
をセットします。
Pkenlm
Psudachi
はkenlm
系のフィルタを入れた時だけ必要。
なお、処理途中のデータを何回か内部で保存してるので、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で保存されるはず。
あとなんかフィルターの各ステージを保存してくれるパラメータがあるはずだが既に思い出せない。(ぉ)
こんな感じでフィルタ別に出力されます
最後のやつだけ出すオプションがあったはずだが思い出せない~
参照
Uzushio: a preprocessing tool for LLM pretraining corpora #Scala - Qiita
WorksApplications/uzushio