🍁

Webスケールの日本語-画像のインターリーブデータセット「MOMIJI」の構築 /巨大テキストデータをAWSで高速に処理するパイプライン

に公開

はじめに

チューリング基盤AIチームの @stu3dio_graph です。

チューリングでは視覚と言語を統合的に理解できるAIを自動運転に応用するため,LLMを視覚モーダルに拡張したVision-Language モデル(VLM)「Heron」の開発に取り組んでいます。

チューリングは経済産業省およびNEDOが推進する日本の生成AIの開発力強化に向けたプロジェクト GENIAC第2期 に採択されました。GENIACでは,完全自動運転を見すえた「身体性」を持つマルチモーダル基盤モデルの開発に取り組みました。日本語環境で高性能なマルチモーダル基盤モデルを作るためには,品質のいい日本語画像およびテキストデータを大量に収集することが不可欠です。

このたび,日本語環境で高性能なVLM; Heron-NVILA-Lite に加えて,Heron-NVILA-Lite の学習のために作成したデータセット; MOMIJI を公開します。モデル自体については 日本語VLM「Heron-NVILA」公開 ─ Qwen2.5-VL-7B・Gemma3-12Bに匹敵する性能 をご覧ください。

https://huggingface.co/datasets/turing-motors/MOMIJI

巨大なテキストデータを効率的に処理するにはエンジニアリング上の工夫が欠かせません。この記事ではペタバイトクラスの巨大テキストデータセットをAWSで高速に処理するためのパイプラインについてご紹介します。データセットとしての学術的な詳細については,後ほど公開する論文をご覧ください。

この記事でわかること

  • 巨大な画像テキストデータデータセット; MOMIJI の概要
  • 巨大な画像テキストデータセット構築のためのエンジニアリング
  • 巨大な画像テキストデータセットを作るためのパイプライン設計

MOMIJI

MOMIJI (Modern Open Multimodal Japanese filtered Dataset) は,大規模かつ厳選された,画像とテキストが交互に現れる(画像-テキストインターリーブ)Web文書の公開データセットです。
2024 年 2 月から 2025 年 1 月までの Common Crawl から抽出されており,約 5,600 万 (56M) 件の日本語文書,約 1,100 億 (110B) 文字,約 2 億 4,900 万 (249M) 枚の画像が含まれています。

類似した他の画像-テキストデータセットと比較した表を以下に示します。我々が構築した MOMIJI は,日本語の画像-テキストインターリーブデータセットでは最大規模であることが分かります。

名前 提供元 形式 言語 画像枚数
llm-jp/relaion2B-en-research-safe-japanese-translation LLM-JP (Sugiura+’25) ペア 日本語 (翻訳) 約2.62B
llm-jp-japanese-image-text-pairs LLM-JP (Sasagawa+’24) ペア 日本語 約6M
llm-jp-japanese-interleaved-data LLM-JP (Sasagawa+’24) インターリーブ 日本語 約6.6M
OBELICS Hugging Face (Laurençon+’23) インターリーブ 英語 約353M
MOMIJI Turing インターリーブ 日本語 約249M

画像-テキストインターリーブデータセット

画像-テキストインターリーブデータセットとは,主にWeb文書から構築された画像とテキストが交互に現れる形式のデータセットのことです。このようなインターリーブ形式のデータを利用することでVLMの性能が向上するかどうかはまだ議論が別れています。インターリーブ形式のデータを用いる効果については別に論文の形で公開する予定です。

以下が実際のMOMIJIのデータです。 <image1><image2> はプレースホルダーであり実際のWebページで画像が挿入されていた位置を示しています。人間がWebページを閲覧するときと同じように,テキストと画像の自然な位置関係を保っているのが特徴です。

MOMIJIには texttext_list は含まれませんが,GitHubに公開している momiji_generator を使うことで,URLからインターリーブ形式の texttext_list が得られます。

過去に公開したデータセットとの比較

チューリングが過去に公開したデータセットである Wikipedia-Vision-JACauldron-JA との比較を以下に示します。Wikipedia-Vision-JAやCauldron-JAは主に200万 (2M) 程度の画像やキャプションを含み,主に事後学習向けのデータセットでした。対照的にMOMIJIはWebスケールの広範な知識を含むより大規模なデータセットであり,VLMモデルの事前学習に使えるのが特徴です。

データセット ソース 主な用途 スケール フェーズ
Wikipedia-Vision-JA 日本語 Wikipedia キャプション生成・検索・Retrieval 1.60 M 画像+キャプション+周辺文 軽量事前学習 / 事後学習
Cauldron-JA 英語V&Lデータセットを翻訳 VQA / OCR / 図表QA など多タスク微調整 1.48 M QA 行、≈1.5 M 画像 事後学習
MOMIJI Common Crawl 基盤 VLM の事前学習 / 継続事前学習 56 M 文書 ≒ 250 M 画像 (URL) 事前学習

MOMIJI構築の全体図

詳細に入る前にMOMIJI構築のための全体図を以下に示します。基本的には元となるデータセット (Common Crawl) から ダウンロード→フィルタリング→パース といった処理を段階的に実施して最終的な出力を整形しています。

このタスクでは以下のような課題がありました。

  • 元となるデータセットが巨大であり,取り回しづらい
  • 画像やテキストを大量にダウンロードし解析する必要がある
  • ひとつひとつのフィルタリングが軽くても,処理対象のファイル数が膨大
  • 多段なフィルタリングを順番に管理し実施していく必要がある
  • GENIACの限られた期間内でデータセットを完成させる必要がある

こういった課題をクラウドをフル活用し AWS Lambda + AWS Step Functions でパイプラインを構築することで解決し,データセットが完成しました。

Common Crawl

Common Crawl は2008年に始まった,世界中のウェブページをクロールし続けている非営利なプロジェクトです。毎月テラバイト級のHTMLを収集し,だれでもダウンロードできる形で収集し公開し続けてくれている「公共ウェブアーカイブ」のような存在です。

データ形式としては

  • WARCファイル (ページ本文+HTTPヘッダ)
  • WETファイル (本文のみ抽出)
  • WATファイル (メタデータ)

となっており,実体はS3のpublic bucketにあります。これらの情報が概ね1ヶ月おき程度で スナップショット として保存されています。これ以降のタスクでは主に WARCファイル を中心に扱います。

Common Crawl January 2025 概要

たとえばスナップショットの一つである CC-MAIN-2025-05 には以下のようなデータが含まれています。

Data Type File List #Files Total Size (TiB, compressed)
Segments segment.paths.gz 100
WARC warc.paths.gz 90 000 93.46
WAT wat.paths.gz 90 000 21.55
WET wet.paths.gz 90 000 8.40
robots.txt robotstxt.paths.gz 90 000 0.16
Non-200 responses non200responses.paths.gz 90 000 3.09
URL index cc-index.paths.gz 302 0.23
Columnar URL index cc-index-table.paths.gz 900 0.26

今回主に使用するWARCファイルは 1 スナップショットでも100TiBとなっており,1年分を処理しようとするとダウンロードだけで単純計算で1PBは軽く超えてしまいます。巨大な Common Crawl全体をどう効率よくフィルタリングしていくか が非常に重要でした。

日本語判定処理とフィルタリングを試す

まずはパイロットスタディとしてTuringの持つオンプレミス環境である Gaggle クラスタで以下のパイプラインを実行してみました。

  1. WARCファイルからのHTMLテキスト抽出
  2. BeautifulSoupでHTMLを解析
  3. 簡易日本語判定
  4. 画像URLの取得とプレースホルダーの挿入
  5. プレースホルダーを含むHTMLからテキスト本文を抽出

このパイプラインを実装し16コアのマルチコアCPUで実行すると,1 warcファイルあたりでだいたい2分程度かかることがわかりました。1スナップショットはwarcファイルを90,000件含むため,

90,000 [] × 2 [min]
= 180,000 [min]
= 3,000 [h]
= 125 [day]
= 4.16 [month]

となり,1ヶ月分を処理するのに4.16ヶ月ほどかかってしまう計算になり現実的ではありません。これをなんとかして高速化する必要があります。

上記のパイロットスタディでの実装を高速化するためにScaleneでプロファイルを実施しました。その結果「BeautifulSoupでHTMLの解析」に多くの時間がかかることがわかりました。実際にはHTMLの <html lang="ja"> といったタグを解析して日本語判定を行うのですが,HTMLをパースする木構造の構築に時間を要していました。

高速に日本語以外を捨てる

Common Crawl は非常に巨大なデータセットではありますが,日本語はその中の5%程度しか含まれていません。高速に日本語かどうかを判定し,その後のフィルタリング処理に受け継ぐことで,大幅に処理時間を削減できそうです。

そのため 簡易かつ高速な日本語判定でとにかく量を減らす ことを目的に最初のパイプラインを構築することを考えました。上流ではRecallを重視して落とし漏れを減らし,下流でより精緻な日本語判定をすればいいからです。

そのために 日本語で書かれているWebページならばひらがな,カタカナ,漢字のいずれかを必ず含む との仮定に基づき簡易な日本語判定を実装しました。またHTMLの lang タグを解析するだけでは日本語判定には不十分であることもわかりました。実際には日本語で書かれているが lang タグが正しく設定されていないWebページ が存在するからです。実際には中国語や韓国語といった漢字を含む語圏のWebページや,他言語圏にたまたま日本語が混入するものもありましたが,そういったものは後段のより精緻なフィルタで除かれます。

import re

class LanguageChecker:
    # Regex pattern for the 'lang' attribute in HTML
    lang_regex = re.compile(r'<html[^>]*\\blang=["\\']?([a-zA-Z-]+)["\\']?', re.IGNORECASE)
    # Regex pattern for Japanese characters (Hiragana, Katakana, or Kanji)
    japanese_text_regex = re.compile(r"[\\u3040-\\u30FF\\u4E00-\\u9FFF]")

    @staticmethod
    def is_japanese_page_by_lang_regexp(html_content: str) -> bool:
        matched = LanguageChecker.lang_regex.search(html_content)
        lang_attr = matched.group(1) if matched else ""
        return "ja" in lang_attr.lower()

    @staticmethod
    def contains_japanese_text(html_content: str) -> bool:
        return bool(LanguageChecker.japanese_text_regex.search(html_content))

これを実装したコードはこのようになります。Python標準の正規表現ライブラリのみを使用しており,非常に高速に動作します。実際にはLambda環境で1 warcファイルあたり2分程度で処理が完了しました。日本語を判定するだけなら16コアのマルチコアCPUで並列実行した場合とほぼ同等の処理速度であり,かなり効果的な高速化だといえそうです。

AWSで高速にスケールするフィルタリングパイプライン設計

このようなひとつひとつの処理は軽いが大量のデータが存在するタスクはAWSやGCPといったクラウド環境で処理するのに非常に適しています。GENIACプロジェクトの限られた期間内で実装しきり完成させるために,なるべくシンプルな構成にすることを心がけて設計しました。先に述べたオンプレ環境で処理することもできますが,

  • Common Crawl の実体がS3に存在すること
  • 1 warc ファイルが 1 URL を持っており静的であること
  • 多段のフィルタリングを組む必要があること
  • 簡単に並列度を数千程度まで上げられること
  • シンプルな設計で済むこと

のような理由で AWS Lambda + AWS Step Functions を中心にパイプラインを設計しました。AWS Batch / ECS Fargate や EMR /Glueを使うことも検討しましたが

  • 1 warcファイルあたりの処理が軽いこと
  • インフラ管理をしなくてもいいこと
  • I/O待ちがなくS3を読んでS3に書けばいいこと
  • 失敗時の再実行もS3 URL単位で十分なこと

あたりの理由からAWSで完結させこのような技術選定としました。

フィルタリングパイプライン構成

実際には以下のようなパイプラインを設計しました。

  1. Common Crawl から Warc ファイルをダウンロードし簡単な日本語判定をしてJSONLにする
  2. fastTextを用いて精緻な日本語判定をする
  3. HTMLをパースして本文を抜き出す
  4. 日本語の品質に基づいたフィルタリングをする
  5. 画像URLに基づいたフィルタリングをする
  6. URLから画像をダウンロードする
  7. 画像の存在とメタデータに基づいたフィルタリングをする
  8. VLMの学習用に整形をする

図に示すと以下のようになります。最初のバッチのみ Common Crawl バケットからデータを取得します。後段のバッチは MOMIJI バケットからデータを取得し,フィルタリングや処理を行って結果を MOMIJI バケットに書き戻すことを繰り返しています。

テキスト情報を用いたフィルタリングが中心ですが,6. URLから画像をダウンロードする7. 画像の存在とメタデータに基づいたフィルタリングをする で画像を使ったフィルタリングも行っているのが特徴です。
6. URLから画像をダウンロードする の部分だけライブラリ依存の関係でAWS Batchを使っていますが,それ以外はすべて AWS Lambda + Step Functions で実装しています。また今回パイプライン構築のために使ったすべてのコードも後ほど公開する予定です。

パイプラインの内部実装

それぞれのパイプラインの内部も構成としては非常にシンプルであり

  1. S3のURLを受け取る
  2. S3からファイルをダウンロードする
  3. フィルタリングの処理を行う
  4. ファイルを書き出して圧縮
  5. S3にファイルをアップロードする

をそれぞれ行うだけです。また 3. フィルタリングの処理を行う 以外の部分はパイプラインごとで共通しているため,クラスに切り出して再利用性を高めています。Lambda関数のエントリーポイントとしてはこのようになります。

def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
    """Lambda function entry point."""

    s3_key, skip = prepare_keys(event)

    file_namager = FileManager(s3_key)
    file_manager.clean_tmp_directory()
    file_namager.prepare_paths(config.paths)

    if message := maybe_skip():
		    return {"status": "Skipped", "message": message}

    file_namager.download_file(config.s3_configs)

    filtered_docs = filter_records(paths.local_path)

    file_manager.finalize_files(filtered_docs)
    file_manager.clean_up_files()

    return {"status": "Completed", "result_path": file_manager.result_path}

このように処理の多くを切り出すことで,メインのロジックである filter_records を書き換えるだけでパイプラインの実装に集中できるようにしました。

def filter_records(
    filepath: str, verbose: bool
) -> Iterator[dict[str, Any]]:
    for record in extract_records_from_jsonl(filepath):
        extracted_text = processor.extract_text(record)
        if not extracted_text:
            continue
        yield processor.generate_output(extracted_text)

filter_records 自体もこのようにシンプルな実装になっており, jsonl からレコードをひとつずつ取り出してフィルタリングを行い,残ったものを yield することでメモリ効率を抑えながら処理を実現しています。

def extract_text(record) -> str | None:
    """
    Extracts Japanese text from the record content.
    """

    content = self.content(record)
    title = self.extract_title(content)

    result = LanguageChecker.contains_japanese_text(str(title))

    if not result:
        return None

    if LanguageChecker.detect_japanese_page_by_regexp(content):
        return content

    self.data["lang_ja_score"] = result["score"]

    return content

これは 精緻な日本語判定をする の処理の一例ですが,実際の判定処理も LanguageChecker に切り出すことで,取り回しやすい設計としています。

Lambdaへのデプロイ

Lambdaへのデプロイ方法は複数存在します。1. Common Crawl から Warc ファイルをダウンロードし簡単な日本語判定をしてJSONLにする のようなほとんど標準ライブラリで動くものであれば,zipファイルで固めてデプロイするのが手軽でした。

しかし fastText , HojiChar , lxml といった外部ライブラリに依存するパッケージを使う場合がやや困難でした。Lambda Layerを使ったり,Amazon Linux を使ってビルドしたwheelをコピーするDockerfileを書いたりなど,Pythonライブラリ依存のデプロイには工夫が必要でした。

Step Functions での実行

今回作ったLambdaは関数1つあたり1URLで動作します。そのLambdaを並列で動かすために Step Functions を利用しました。 Map に直接S3 prefixを渡すとその下のkeyをすべて読み,それぞれのLambdaに配ります。今回は1つのStep Functionsあたりで90,000のURLを含むため分散モードでMap関数を利用します。

静的なS3 URLを読み込んで静的なS3 URLを返すだけなので,状態管理やI/O待ちをしなくていいこともかなりシンプルな設計をするのに便利でした。なにかしらの理由で処理が完了しなかった場合はS3 URLが存在しないだけだからです。リトライも単純にジョブを再実行するだけで済みました。

またなにかと複雑になりがちなAWSポリシー管理も

  • LambdaにはS3の読み書き
  • Step FunctionsにはS3の読み出しとLambda実行

の権限を付与するだけで済みました。

高い並列度での実行

それぞれの処理を行う Step Functions が完成したら,あとは Step Functions を呼び出す Step Functions を書くだけです。今回はそれぞれのパイプラインで 4,000並列でLambdaを実行 しています。その結果,画像ダウンロードを含めても 1スナップショット あたり6〜8.5時間 程度でジョブが完了します。

MOMIJIは実際には11スナップショットで構築される大規模なデータセットでありながら,計算時間としては4営業日程度で処理が完了しました。

パイプライン設計から得られた知見

今回の設計で,なるべくシンプルかつ高速に処理するパイプラインをうまく構築できました。このような規模のバッチ処理システムをAWSで構築するのは私にとってはじめての経験でしたが,実際に数ヶ月で実用的なパイプラインを構築できました。

うまくいったこと

フィルタリングの処理の単位にわけてパイプラインを設計し実装することは非常にうまくいきました。それぞれの処理同士に依存がなかったことが大きな要因です。またいきなり数万程度のwarcファイルを処理するのではなく,1warcファイルに分離したLambda関数として実装したのもいい選択でした。処理時間やコストを見積もったり,スケーリングさせたりすることをクラウドの力を使ってスムーズに行えました。

うまくいかなかったこと

Pythonライブラリや環境の依存問題には苦しめられました。シンプルに実装するためにLambdaを選択したはずが,いくつかのライブラリがLambda環境や特定のPythonバージョンでは動かずに大変苦労しました。LambdaではPythonの並列処理関連は実行環境レベルで利用できないようになっていますが,ライブラリによっては要求してくるものもありました。最終的にはDockerfileやモンキーパッチを書くことで解決しましたが,余計な時間を使ってしまいました。こういった問題はPythonを使っているとよく発生しますが,設計段階で察知するのは大変困難でした。

MOMIJIデータセットの規模

以下にMOMIJIデータセットの規模を示します。数億枚の画像を持つ数千万ものWebページを,わずか数日で収集できました。これはAWSを使わなければ達成できない規模です。

指標 数値
収集されたWebページ数 56,119,639
収集された画像数 249,745,953
収集されたテキストの文字数 109,980,725,957
Webページあたりの平均文字数 1,959
Webページあたりの平均画像数 4.45

おわりに

この記事では巨大な画像-テキストデータセットを構築するためのパイプラインについて説明しました。AWSを活用して日本語判定とフィルタリングをどのように効率化するかがおわかりいただけたでしょうか。今回作成したデータセットを使って学習したVLMモデルである Heron-NVILA も非常に高い性能を誇っており,高品質かつ大規模な画像-テキストデータセットが,VLMの性能向上にも非常に重要であることがわかります。

私の所属する基盤AIチームでは,VLMや動画生成といったAIモデルの開発以外にも,このようなエンジニアリングタスクが多数存在しています。AIエンジニアだけではなくソフトウェアエンジニアにとっても大変チャレンジングな環境です。みなさまの応募をお待ちしています。

https://tur.ing/jobs

Tech Blog - Turing

Discussion