Generative AI

NeMo Curator を使った日本語データのキュレーション – モデルベース編

Reading Time: 4 minutes

本記事では、NeMo Curator を使った日本語データのキュレーションに続き、NeMo Curator を使用して日本語データセットの品質を向上させるアプローチをご紹介します。

NeMo Curator とは

NeMo Curator は、大規模言語モデル (LLM) の事前学習、text-to-image モデルの学習、ドメイン適応型事前学習 (DAPT)、教師ありファインチューニング (SFT)、Parameter-Efficient Fine-Tuning (PEFT) などの生成 AI ユース ケース向けに高速でスケーラブルなデータセットの準備とキュレーションを行うために設計された Python ライブラリです。

DaskRAPIDS で GPU を活用することでデータ キュレーションを大幅に高速化し、大幅な時間の短縮を実現します。このライブラリはカスタマイズ可能なモジュール式インターフェイスを提供し、パイプラインの拡張を簡素化し、高品質のトークンを準備することでモデルの収束を加速します。

NVIDIA NeMo の一部である NeMo Curator は、Common Crawl、Wikipedia、arXiv などのパブリック ソースからすぐにデータをダウンロードしてキュレートするためのワークフローを提供します。また、開発者の独自の要件にカスタマイズされたデータ キュレーション パイプラインにより、簡単にカスタム データセットを作成できる柔軟性も提供します。

現在、NeMo Curator のテキスト キュレーションですぐに使用できる機能として以下を提供しています。

  • データのダウンロードとテキストの抽出
  • 言語の識別と分離
  • テキストの再フォーマット化とクリーニング
  • 品質フィルタリング
  • ドキュメントレベルの重複排除
  • 多言語ダウンストリーム タスクの除染 (評価で使用するデータが学習データ内に重複して含まれていないかを確認し、重複している場合はそれを排除するプロセス)
  • 分散データ分類 (ドメイン、品質、安全性、コンテンツ分類等)
  • 個人識別情報 (PII) の削除

下図ではそれぞれのステップとそこで使われている主なテクノロジが記載されています。緑で塗りつぶされたステップでは、GPU を使って処理を大幅に高速化することが可能です。

図 1. NeMo Curator が提供する機能

NeMo Curator は GitHub 上にあるリポジトリ から、または NeMo Framework のコンテナーからすぐに始めることができます。

NeMo Curator チュートリアル

本記事では、以下の 3 つのテキスト キュレーションをご紹介します。

  • ドキュメントの品質フィルタリング
  • ドキュメントのドメイン分類
  • 個人識別情報 (PII) の削除

それぞれは独立したコンテンツとなっているため、関心のあるチュートリアルだけを実行することができます。

また、今回のチュートリアルの検証環境は以下の条件で行っております。こちらの構成は一例であり、NeMo Curator は Volta 以降の任意の NVIDIA GPU、任意の計算ノードでジョブを実行することが可能です。

  • ハードウェア
    • DGX A100
    • GPU: 1 x NVIDIA A100 80 GB GPU (driver version: 535.183.08)
    • CPU: AMD EPYC 7V12 64-Core Processor
    • システム メモリ: 1 TB
  • ソフトウェア
    • OS: Ubuntu 22.04.4 LTS
    • Container: nvcr.io/nvidia/nemo:25.04

事前準備

以下のコマンドで作業用のディレクトリを作成し、移動します。

mkdir curator-example
cd curator-example

Docker コンテナーの起動

以下のコマンドでコンテナーを起動します。

sudo docker run --rm -it --gpus all --shm-size=64g --ulimit memlock=-1 --network=host -v ${PWD}:/workspace -w /workspace nvcr.io/nvidia/nemo:25.04 bash

データのダウンロード

「ドキュメントの品質フィルタリング」と「ドキュメントのドメイン分類」で使用するデータを事前にダウンロードします。このチュートリアルでは、以下のコマンドで LLM-jp が公開している LLM-jp Corpus v4 に含まれている ja_fineweb-2 から 1 ファイルをサンプルとしてダウンロードします。

mkdir -p data && cd data && wget -qO- --no-check-certificate "https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v4/-/raw/main/ja/ja_fineweb-2/0000.jsonl.gz?ref_type=heads" | gunzip > 0000.jsonl && cd ..

ドキュメントの品質フィルタリング

このセクションでは日本語ドキュメントの品質フィルタリングを紹介します。

品質フィルタリングが求められる背景には、大規模なデータセット、特にインターネットから収集されたデータには「低品質」と見なされるドキュメントが多数含まれているという現状があります。ここでいう「低品質」データとは、モデルのダウンストリーム タスクのパフォーマンスに悪影響を与える、すなわちモデルに学習させたくないデータを指します。そのため、一般的に大規模なデータセットに対して品質フィルタリングを行い、品質の高いデータのみを抽出することが重要とされています。

品質を定義する指標はさまざまで、ドキュメントに含まれる句読点の数、ドキュメントの長さ、ドキュメントの繰り返し頻度といった単純な統計情報から品質を測定するヒューリスティックなアプローチや品質が高い、低いとラベル付けされたデータで分類器を学習し、分類器の出力から測定するモデルベースのアプローチなどがあります。NeMo Curator では、ヒューリスティック、モデルベースのどちらもサポートしており、ここではモデルベースを取り上げます。

このチュートリアルでは、東京科学大学が公開している tokyotech-llm/edu-classifier を使用します。tokyotech-llm/edu-classifier は日本語ドキュメントの教育的価値を判断するための fastText 分類器で、Wiki ベースの分類器 (日本語 Wikipedia の学術カテゴリのテキストで学習)と LLM ベースの分類器 (LLM によって生成された教育的価値のアノテーションで学習) の 2 つ分類器があります。ここでは、より広範なドキュメントに適切な教育的スコアを割り当てたい場合に推奨されている LLM ベースの分類器を例として使用します。

まず、Hugging Face のリポジトリから tokyotech-llm/edu-classifier をダウンロードします。

git lfs install
git clone https://huggingface.co/tokyotech-llm/edu-classifier

次に品質フィルタリングを実行してみましょう。以下が実行スクリプトになります。ベースにしたスクリプトは NeMo Curator のリポジトリ内の examples/classifier_filtering.py にありますが、fastText の学習部分の省略や分類器の出力後の処理、フィルタリング手法などに変更を加えています。min_cutoff の値を変更することでフィルタリング閾値を超えているドキュメントのみを出力することが可能です (以下は閾値を -1.0 としてドキュメントのフィルタリングを行わない設定です)。

import argparse

import fasttext
import numpy as np
import pandas as pd

import nemo_curator as nc
from nemo_curator.datasets import DocumentDataset
from nemo_curator.filters.doc_filter import DocumentFilter
from nemo_curator.utils.decorators import batched
from nemo_curator.utils.distributed_utils import (
    get_client,
    read_data,
    write_to_disk,
    NoWorkerError,
    load_object_on_worker,
)
from nemo_curator.utils.file_utils import get_all_files_paths_under
from nemo_curator.utils.script_utils import ArgumentHelper


def load_dataset(input_data_dir: str) -> DocumentDataset:
    files = list(get_all_files_paths_under(input_data_dir, keep_extensions="jsonl"))
    raw_data = read_data(files, file_type="jsonl", backend="pandas", add_filename=True)
    return DocumentDataset(raw_data)


class LLMFastTextQualityFilter(DocumentFilter):
    def __init__(
        self,
        model_path: str | None = None,
        min_cutoff: float = -1.0,
    ):
        if model_path is None:
            msg = "Must provide a valid path to a FastText model to compute document scores with this filter"
            raise ValueError(msg)
        self._model_path = model_path
        self._cutoff = min_cutoff
        self._name = "fasttext_quality_filter"

    @batched
    def score_document(self, df: pd.Series) -> pd.Series:
        model_attr = f"{self._name}_{self._model_path}"
        try:
            model = load_object_on_worker(model_attr, self._load_model, {})
        except NoWorkerError:
            return pd.Series(np.ones(len(df)), dtype=float)

        def _score_document(text: str) -> float:
            text = text.replace("\n", " ").replace("__label__", " ")
            pred = model.predict(text, k=-1)
            return sum([int(label[-1]) * prob for label, prob in zip(pred[0], pred[1])])

        return df.apply(_score_document)

    @batched
    def keep_document(self, df: pd.Series) -> pd.Series:
        return df > np.full(len(df), self._cutoff)  # noqa: NPY002

    def _load_model(self) -> fasttext.FastText:
        return fasttext.load_model(self._model_path)


def main(args: argparse.Namespace) -> None:
    # Params
    model_path = "edu-classifier/llm_llama.bin"  # "edu-classifier/wiki.bin"
    data_path = "data/"
    filtered_output = "result/0000_scored.jsonl"

    # Prepare samples for the classifier
    client = get_client(**ArgumentHelper.parse_client_args(args), memory_limit="32GB")

    print("Loading dataset...")
    # Filter data
    target_dataset = load_dataset(data_path)
    print(f"Dataset loaded: {len(target_dataset.df)} rows")
    filter_pipeline = nc.ScoreFilter(
        LLMFastTextQualityFilter(model_path, min_cutoff=-1.0),
        score_field="quality_score",
        score_type=float,
    )
    print("Applying filter...")
    filtered_dataset = filter_pipeline(target_dataset)
    print("Writing filtered dataset...")
    write_to_disk(filtered_dataset.df, filtered_output, write_to_filename=True)
    print("Dataset written successfully!")

    client.close()


def attach_args(
    parser: argparse.ArgumentParser,
) -> argparse.ArgumentParser:
    return ArgumentHelper(parser).add_distributed_args()


if __name__ == "__main__":
    main(
        attach_args(
            argparse.ArgumentParser(
                formatter_class=argparse.ArgumentDefaultsHelpFormatter
            )
        ).parse_args()
    )

実行結果を保存する result ディレクトリを作成、上記コードをclassifier_filtering.pyとして保存し、実行します。

mkdir result
python classifier_filtering.py 

実行が完了したら出力された result/0000_scored.jsonl から最も高品質とスコアリングされたテキストを見てみましょう。

jq -s 'max_by(.quality_score)' result/0000_scored.jsonl

"text": "妊婦さんや母乳育児中のママさんがよく気になるのが\"カフェインの摂取\"。もともとコーヒーを飲むのが習慣になっている妊婦さんの中にも、「妊娠中はコーヒーが飲めないのが辛い…」という方が少なくありません。\nそこで今回の記事では、妊婦さんでも安心なカフェインレスコーヒーの選び方について、詳しくご紹介したいと思います。
=== 省略 ===
\n今回の記事を参考にしながら選べば、安心で味もおいしいカフェインレスコーヒーが見つけられるので、妊娠&授乳中でもリラックスできるコーヒータイムを楽しみましょう。",
"quality_score": 2.9894943333

Wiki ベースの分類器についても、読み込むモデルと出力後の処理を少し変更するだけで利用することが可能です。

また、英語のドキュメントの場合は、Hugging Face 上の NeMo Curator – Classifier Models にある複数のモデルが利用可能です。実装方法はドキュメントを参考にしてください。

ドキュメントのドメイン分類

このセクションでは日本語ドキュメントのドメイン分類を紹介します。

大規模なデータセットの中から、特定のドメインに限定したデータの抽出や排除、または特定ドメインにデータが偏っている際にそれを緩和するためにサンプリングを行いたいケースがあります。NVIDIA では日本語を含む 52 の言語と以下の 26 のドメイン カテゴリに対応した多言語ドメイン分類モデルを公開しており、本チュートリアルではこのモデルを使用する例を紹介します。

'Adult', 'Arts_and_Entertainment', 'Autos_and_Vehicles', 'Beauty_and_Fitness', 'Books_and_Literature', 'Business_and_Industrial', 'Computers_and_Electronics', 'Finance', 'Food_and_Drink', 'Games', 'Health', 'Hobbies_and_Leisure', 'Home_and_Garden', 'Internet_and_Telecom', 'Jobs_and_Education', 'Law_and_Government', 'News', 'Online_Communities', 'People_and_Society', 'Pets_and_Animals', 'Real_Estate', 'Science', 'Sensitive_Subjects', 'Shopping', 'Sports', 'Travel_and_Transportation'

それではさっそくドメイン分類を実行してみましょう。以下が実行スクリプトになります。スクリプトは NeMo Curator のリポジトリ内の examples/classifiers/multilingual_domain_example.py をデータの入出力パスを設定するだけでそのまま使用できます。MultilingualDomainClassifierfilter_by 引数で指定したドメインのドキュメントのみを出力することもできます。以下では、スポーツのドメインのみを出力してみます。

import argparse
import time

from nemo_curator.classifiers import MultilingualDomainClassifier
from nemo_curator.datasets import DocumentDataset
from nemo_curator.utils.distributed_utils import get_client
from nemo_curator.utils.script_utils import ArgumentHelper


def main(args: argparse.Namespace) -> None:
    global_st = time.time()

    # Input can be a string or list
    input_file_path = "data/0000.jsonl"
    output_file_path = "result/0000_domain_sports.jsonl"

    client_args = ArgumentHelper.parse_client_args(args)
    client_args["cluster_type"] = "gpu"
    client = get_client(**client_args)

    input_dataset = DocumentDataset.read_json(
        input_file_path, backend="cudf", add_filename=True
    )

    multilingual_domain_classifier = MultilingualDomainClassifier(
        filter_by=["Sports"]
    )
    result_dataset = multilingual_domain_classifier(dataset=input_dataset)

    result_dataset.to_json(output_path=output_file_path, write_to_filename=True)

    global_et = time.time()
    print(
        f"Total time taken for multilingual domain classifier inference: {global_et - global_st} s",
        flush=True,
    )

    client.close()


def attach_args(
    parser: argparse.ArgumentParser,
) -> argparse.ArgumentParser:
    arg_helper = ArgumentHelper(parser)
    arg_helper.add_distributed_classifier_cluster_args()

    return arg_helper.parser


if __name__ == "__main__":
    main(
        attach_args(
            argparse.ArgumentParser(
                formatter_class=argparse.ArgumentDefaultsHelpFormatter
            )
        ).parse_args()
    )

Dask が処理完了後にエラーを出力することがありますが、Total time taken for multilingual domain classifier inference: ○○○ s と表示されていればフィルタリング処理は正常に完了しています。

実行が完了したら出力された result/0000_domain_sports.jsonl の一部を見てみましょう。スポーツ ドメインのテキストが抽出されています。

head -n1 result/0000_domain_sports.jsonl

"text":"スポーツをしていてこんなお悩みはありませんか?\n- もっと力強く動きたい\n- もっと素早く動きたい\n- フォームをきれいに改善したい\n- もっとボールを飛ばしたい\n- もっと持久力を上げたい\n- もっと体を柔軟にしたい\nこのような願望を叶えてくれるメソッド(技法)があります。
=== 省略 ===
\nあなたにはもっとスポーツを楽しんで、強くなっていただきたい、それが私の希望です。そのためにこの技術を大いに活用し、出し惜しみしないことをお約束します。","domain_pred":"Sports"

個人識別情報 (PII) の削除

このセクションでは NeMo Curator による個人識別情報の削除機能を紹介します。

大規模なテキスト データを取り扱う上で、ユーザーのプライバシー保護とデータ セキュリティの確保は極めて重要です。特に AI アプリケーションの開発やデータセットの共有/公開の場面では、個人を識別できる情報 (PII: Personally Identifiable Information) を正確に検出し、適切に削除やマスキングすることが、コンプライアンス対応や信頼性の維持に不可欠です。

このようなニーズに応えるために、NeMo Curator で PII の削除機能を提供しており、氏名や連絡先情報、住所、金融関連情報など、幅広い個人識別情報の検出と削除に対応しています。

NeMo Curator における PII の検出と処理には、主に 2 種類のアプローチがあります。ひとつはルールベースおよび従来のパターン マッチング手法を用いた Presidio による方法で、高精度かつ制御性が高く、大規模なバッチ処理に適しています。もうひとつは大規模言語モデルを活用した手法で、文脈が複雑でルールでは対応しきれないようなケースでの識別に適しています。

  • ルールベースの Presidio 手法
    Microsoft のオープンソース ライブラリ Presidio を用いて、テキスト中の個人識別情報を検出/削除します。本質的にはパターン マッチングの手法を用いており、統計や機械学習モデルの技術を活用することで、ユーザーは独自のルールを設定して、精度の高いマッチングを行うことが可能です。NeMo Curator では、Dask を併用することで、数テラバイト規模の大規模データにもスケーラブルに対応可能です。具体的な実装については、「PII redaction using Presidio」に関するドキュメントをご覧ください。
  • 大規模言語モデル (LLM) を用いた PII 識別
    NVIDIA NIM を通じて、最新の LLM を活用することで、ユーザーはニーズに応じた言語モデルを選択しつつ、複雑な文脈における PII の識別にも柔軟に対応することが可能になります。 例として、日本語の PII 識別には、日本語に特化した tokyotech-llm/llama-3-swallow-70b-instruct-v0.1 モデルなどが活用することができます。

同時に、Nemo Curator の PII ツールは、簡単な設定で実現できます。例えば、以下のとおりの PII エンティティを指定することで、必要なエンティティを検出できるようになります。

[
    "medical_record_number",
    "location",
    "address",
    "date_of_birth",
    "date_time",
    "name",
    "email",
    ...
]

以下では、LLM 手法を用いて PII を検出し、マスキングする方法についての簡単なチュートリアルを紹介します。

大規模言語モデル (LLM) を用いた PII 識別

NeMo Curator は本質的に、大規模言語モデル (LLM) を非同期かつ拡張可能なテキスト処理モジュールとして組み込み、構造化されたシステム プロンプト (system prompt) やタスク テンプレート (task template) を入力データと組み合わせて、分散データ フロー上で命令ベースの推論 (instruction-based inference) をバッチ処理として実行します。これにより、データのクレンジング、アノテーション、フィルタリングといった高レベルな意味処理が可能になります。

ただし、デフォルトのシステム プロンプトは英語ベースであるため (サンプル データについては、こちらの資料をご参照ください)、日本語のコンテンツに対してより適切に PII を識別するには、システム プロンプトなどをはじめとする設定を日本語の指示に置き換える必要があります。

import pandas as pd
import textwrap

from nemo_curator.datasets import DocumentDataset
from nemo_curator.modifiers.async_llm_pii_modifier import AsyncLLMPiiModifier
from nemo_curator.modules.modify import Modify
from nemo_curator.utils.distributed_utils import get_client
import multiprocessing as mp

def main():
    # 準備: Daskクライアント(分散実行/スケールアウト用、ローカルでもOK)
    client = get_client(cluster_type="gpu", n_workers=8)
    
    # LLMの出力を機械可読に保つため、返却形式を固定化
    JSON_SCHEMA = {
        "type": "array",
        "items": {
            "type": "object",
            "required": ["entity_type", "entity_text"],
            "properties": {"entity_type": {"type": "string"}, "entity_text": {"type": "string"}},
        },
    }
    
    # PIIエンティティを明示
    EVAL_LABELS = ["name","email","address","phone_number","employee_id","location","customer_id","ip_address","password","credit_card_number","api_key"]
    
    # ​​システムプロンプト: 役割指示(編集者)/ 出力制約(JSON)/ 非正規化(原文コピー)を明記
    # スキーマ/ラベルを文字列で埋め込み、LLM側の解釈余地を最小化
    SYSTEM_PROMPT = textwrap.dedent(f"""
        あなたは熟練の編集者です。ユーザーがテキストを提供しますので、
        その中から個人を特定できる情報(PII)を見つけてください。
    
        重要: 出力は必ず次のJSONスキーマに従ってください:
        {JSON_SCHEMA}
    
        厳守事項:
        - 実際に入力テキストに含まれている情報のみを返してください(捏造禁止)
        - entity_text は入力から正確にコピーし、変更・正規化をしてはいけません
        - entity_type は次のいずれかに限定します: {", ".join(EVAL_LABELS)}
    """ ).strip()
    
    # Input: テスト用ダミーデータの準備
    rows = [
        {
            "text": ("木下大地(ユーザー名: daichi.k、顧客ID: C23456)は、自宅のIPアドレス(172.16.0.51)からWebサービスにアクセスし、新しいパスワード「SafePass2025」に変更した後、アカウント情報を最新のものに更新しました。")
        },
        {
            "text": ("石川直樹(社員番号: EMP-33322、クレジットカード番号: 4000-1111-2222-3333)は、2025年6月18日午後3時ごろ、会社の専用システムにAPIキー「APIKEY-ISH2025」を入力し、オンライン経費精算用にアカウントにログインしました。その際、本人確認のため追加の認証も行いました。")
        },
    ]
    
    dataframe = pd.DataFrame(rows, columns=["text"]).reset_index(drop=False).rename(columns={"index": "id"})
    
    dataset = DocumentDataset.from_pandas(dataframe, npartitions=1)
    
    # LLMモディファイアの設定(非同期)
    modifier = AsyncLLMPiiModifier(
        # Endpoint for the user's NIM
        base_url="https://integrate.api.nvidia.com/v1",
        api_key="<your_nvidia_api_key>",  # 取得したAPIキーを入力してください
        model="tokyotech-llm/llama-3-swallow-70b-instruct-v0.1",
        system_prompt = SYSTEM_PROMPT,
        language="jpn",
        max_concurrent_requests=10,
    )
    
    modify = Modify(modifier)
    modified_dataset = modify(dataset)
    
    # Output
    modified_dataset.to_json("output.jsonl")
    client.close()


if __name__ == "__main__":
    mp.freeze_support()
    main()

モデルの利用に必要な API キーは、build.nvidia.com を通じて取得可能です。

最終的な出力結果の JSON は以下のとおりで、あらかじめ指定した PII エンティティがマスクされる形になります。

そして出力結果について、LLM を用いた手法であるため、結果は実行のたびに変動する可能性があり、利用するモデルやプロンプト設計によって精度や再現性に差異が生じることがあります。

# output.jsonl

{"id":0,"text":"{{name}}(ユーザー名: daichi.k、顧客ID: {{customer_id}})は、自宅のIPアドレス({{ip_address}})からWebサービスにアクセスし、新しいパスワード「{{password}}」に変更した後、アカウント情報を最新のものに更新しました。"}

{"id":1,"text":"{{name}}(社員番号: {{employee_id}}、クレジットカード番号: {{credit_card_number}})は、2025年6月18日午後3時ごろ、会社の専用システムにAPIキー「{{api_key}}」を入力し、オンライン経費精算用にアカウントにログインしました。その際、本人確認のため追加の認証も行いました。"}

まとめ

本記事では、NeMo Curator を使用したモデルベースのテキスト データの品質向上アプローチを紹介しました。NeMo Curator がカスタム モデルの開発をより効率化、改善できると嬉しいです。


関連情報

Tags