1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tatoebaから言語学的3軸でバランスさせた日本語1万文データセットの作り方(ja_balanced_10000.csv)

Last updated at Posted at 2025-10-11

概要

  • 本記事では、Tatoeba の日本語例文コーパスから高品質な日本語文を抽出し、文の機能×丁寧度×複雑度の3軸でバランスさせたデータセット ja_balanced_10000.csv(10,000文・1列CSV)を作成するまでの設計思想・実装・検証方法を丁寧に解説します。
  • 背景として、JParaCrawl 由来の文に見出し的・断片的なノイズが残りやすい課題があり、今回は Tatoeba の sentences.csv に絞って品質とバランスを両立しました。

成果物

  • 生成物: ja_balanced_10000.csv(UTF-8、1列ヘッダ ja、10,000行)
  • 文字数レンジ: 20〜50 文字(調整可能)
  • バランス軸: 文の機能(5種)×丁寧度(3種)×複雑度(3種)=最大45バケットから quota 抽出+不足分をプール補充
  • 再現性: 乱数シード固定(デフォルト 42)
  • ログ/統計: 実行時に詳細な処理統計と最終分布を標準エラーに出力

背景と設計思想

  • 目的: 学習・評価に使えるバランスの良い中短文コーパスを高速・再現可能に作る
  • 過去課題: Web 由来の JParaCrawl は厳格フィルタでも「見出しっぽい断片」「広告文」「書式ノイズ」が残存
  • 戦略: 人手ベースで比較的自然な Tatoeba に限定し、文末・構文のシグナルを用いた3軸分類で多様性を担保

データソースとライセンス

  • ソース: Tatoeba Exports sentences.tar.bz2(中に sentences.csv
  • 形式: タブ区切り TSV(id, lang, text)
  • ライセンス: CC BY 2.0(帰属が必要)
  • 注意: 配布・公開時は Tatoeba へのクレジット(サイト/プロジェクト名、CC BY 2.0)を明記してください

環境準備

  • 前提: Python 3.7+(標準ライブラリのみで可)
  • データ取得
wget https://downloads.tatoeba.org/exports/sentences.tar.bz2

# 解凍(約10秒)
tar -xjf sentences.tar.bz2

# 日本語行の確認(例)
grep $'\t''jpn'$'\t' sentences.csv | head -3

抽出スクリプトの要点

  • 実行ファイル: extract_ja_perfect.py
  • 主要処理フロー:
    • 入力(TSV)読み込み → 品質フィルタ → 3軸特徴抽出 → バケット投入
    • quota 抽出(バケット均等取り)→ 余剰プールから補充 → シャッフル → 書き出し
    • 詳細統計の生成(長さ分布・各軸分布・上位組み合わせ)

品質フィルタリング(実装抜粋)

  • 目的: 露骨なノイズ・不自然文を除外し、レンジ内に統一
  • ルール:
    • URL/メンション/ハッシュタグ/絵文字/HTML タグを除外
    • 同一文字の長い連続(スパム)を除外
    • 日本語文字(ひらがな・カタカナ・漢字)を含まない行は除外
    • 文字数 20〜50 に制限(可変)
if re.search(r'https?://', text):
    return False, "contains_url"
if re.search(r'[@@##]', text):
    return False, "contains_mention_or_hashtag"
if re.search(r'[\u2600-\u27BF\U0001F300-\U0001F9FF]', text):
    return False, "contains_emoji"
if re.search(r'<[^>]+>', text):
    return False, "contains_html_tag"
if not re.search(r'[ぁ-んァ-ヴー一-龥]', text):
    return False, "no_japanese_chars"
if re.search(r'(.)\1{4,}', text):
    return False, "repetitive_chars"
if not (min_len <= len(text) <= max_len):
    return False, "len_out_of_range"

3軸分類(言語学的特徴)

  • 文の機能(Function): question / statement / imperative / conditional / volition
    • 指標例: 疑問符、終助詞「か」、命令・依頼(〜しろ/してください/〜なさい)、条件節(〜なら/たら/れば)、意志・推量(〜よう/だろう/でしょう/〜たい)
  • 丁寧度(Politeness): polite / plain / other
    • 指標例: 文末の「です・ます」「だ・である」活用
  • 複雑度(Complexity): simple / complex / enumeration
    • 指標例: 読点と接続表現(読点2つ以上は列挙)
# function
r'[??]'; r'か[。\s]*$'
r'(なさい|ください|てくれ|しろ|せよ|するな)[。!!]*$'
r'(なら|ならば|たら|れば|ば)[、。]'
r'(たい|たく|たがる)[。、!!]' ; r'(よう|まい|だろう|でしょう)[。!!]*$'

# politeness
r'(です|ます|ございました)[。!?!?、]*$'
r'(だ|だった|である|であった|だろう)[。!?!?]*$' ; r'(た|ない|なかった)[。!?!?]$'

# complexity
r'、.+、.+'            # enumeration
r'' ; r'(ので|のに|から|けれど|が、|し、|て、|で、)'  # complex

バランス抽出アルゴリズム

  • 45バケット(5×3×3)を動的に作成
  • 各バケットの件数から quota = target_n // num_buckets を算出して均等に配分
  • quota 未達の不足分は、全余剰文のプールからランダム補充
  • 乱数シード固定で再現性を確保し、最後に全体シャッフルして出力

実行方法

  • デフォルト(10,000文、20–50文字、出力先 ja_balanced_10000.csv)
python extract_ja_perfect.py --verbose
  • パラメータ変更例
# 短文コーパス(10–30文字)
python extract_ja_perfect.py \
  --n 5000 --min_len 10 --max_len 30 \
  --out ja_short.csv --verbose

# 長文コーパス(50–100文字)
python extract_ja_perfect.py \
  --n 3000 --min_len 50 --max_len 100 \
  --out ja_long.csv --verbose

実行ログの読み方(要点)

  • 入力・出力・ターゲット数・文字数レンジ・シードの表示
  • 読み込み進捗(オプション)
  • 日本語行総数、受理数、除外理由内訳(URL/絵文字/短すぎ/長すぎ/重複/HTML 等)
  • バランス抽出の配分(verbose 時に各バケットの候補数→採択数)
  • 最終統計(長さ min/max/mean/median、各軸の分布、上位組み合わせ)

結果の一例(10,000文・20–50文字)

  • 文字数: min 20 / max 50 / mean 約27 / median 25
  • 文の機能: statement 4,861、question 1,721、volition 1,563、conditional 1,225、imperative 630
  • 丁寧度: polite 2,731、plain 3,156、other 4,113
  • 複雑度: simple 4,004、complex 4,364、enumeration 1,632
  • 備考: 実データにより一部バケットが希少(特に命令など)。quota 未達分はプール充填で補うため、完全均等にはならないのが現実的な落としどころ

検証チェックリスト

  • 行数: wc -l ja_balanced_10000.csv が 10,001(ヘッダ含む)
  • 文字数制約: 任意サンプルで 20 <= len(s) <= 50 を確認
  • 目視: 100件ほど抽出して、不自然な見出し・広告・書式崩れがないか
  • 分布: 軸別集計が用途に合うか(例: QA 系で疑問文比率を増やしたい等)

JParaCrawl を使わなかった理由と現行の評価

  • 過去: JParaCrawl は厳格フィルタでも Web 見出し・広告断片が混入しやすく、学習時のスタイル汚染が懸念
  • 現行: Tatoeba 限定+3軸分類で、自然な文体と多様な構文・機能を確保しつつ、単純な表層ノイズを広範に排除
  • 残課題: 命令文・受動構文・会話引用など、希少カテゴリの底上げが必要な場合は追加のサンプリング制約や補助コーパスを検討

カスタマイズのヒント

  • 最低/最大割合制約の導入(例: imperative を最低 8%、polite を 30%±5%)
  • 形態素解析の併用(文末表現の誤検知を品詞タグで補正)
  • 意味的重複の抑制(n-gram に加え、埋め込み類似度によるパラフレーズ抑制)
  • トピック分散(キーワード辞書や軽量分類器でジャンル偏りを平準化)
  • 出力メタ列の追加(source/type 列で学習時の条件付けを可能に)

トラブルシューティング

  • sentences.csv が見つからない → wget 展開、パス指定 --src sentences.csv を再確認
  • 10,000文に届かない → --min_len / --max_len を広げる(例: 15–60)、または --n を一時的に減らす
  • 文字化け → UTF-8 を確認。Excel で開く場合はインポート設定に注意
  • 偏りが強い → quota 設計を見直す、または不足カテゴリの優先スコアリングを導入

ライセンスと帰属表示(重要)

  • Tatoeba: CC BY 2.0
  • 公開・配布時は Tatoeba への Attribution を明記してください
    • 例: “Contains data from Tatoeba Project, licensed under CC BY 2.0”

まとめ

  • Tatoeba ベースで品質フィルタ→3軸特徴抽出→バランス抽出→統計検証までの一連の工程を解説しました。
  • ポイントは「表層ノイズを強く除去」「文法的シグナルに基づく3軸バランス」「再現性確保」の3つ。
  • ニーズに応じて quota と判定式を調整すれば、チャット用短文・読解用長文など、目的別コーパスを同じフレームで量産できます。

全文コード(extract_ja_perfect.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tatoeba日本語文抽出スクリプト【完全版】

言語学的に妥当な3軸分類でバランスの取れたデータセットを作成:
  1. 文の機能(質問/平叙/命令/条件/推量)
  2. 丁寧度(丁寧体/普通体/その他)
  3. 文の複雑度(単文/複文/列挙)

Author: Claude
Version: 2.0
"""

import csv
import re
import random
import argparse
import sys
from collections import defaultdict, Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Dict, Tuple, Optional
import statistics


# ============================================================================
# データクラス定義
# ============================================================================

@dataclass
class SentenceFeatures:
    """文の言語的特徴を保持するデータクラス"""
    text: str
    
    # 主要な分類軸(バランス抽出に使用)
    function: str       # 文の機能: question/statement/imperative/conditional/volition
    politeness: str     # 丁寧度: polite/plain/other
    complexity: str     # 複雑度: simple/complex/enumeration
    
    # 補助的特徴(統計情報として記録)
    has_quote: bool = False      # 引用符の有無
    has_numeric: bool = False    # 数値の有無
    has_katakana: bool = False   # カタカナ語(3文字以上)の有無
    
    char_count: int = 0          # 文字数
    
    def get_bucket_key(self) -> Tuple[str, str, str]:
        """バランス抽出用のバケットキーを返す"""
        return (self.function, self.politeness, self.complexity)


# ============================================================================
# 分類器クラス
# ============================================================================

class JapaneseSentenceClassifier:
    """
    日本語文を言語学的特徴に基づいて分類
    
    正規表現は慎重に設計され、誤検知を最小限に抑えている。
    """
    
    def __init__(self):
        # 文の機能を判定する正規表現(優先順位順)
        self.function_patterns = {
            'question': [
                re.compile(r'[??]'),                    # 疑問符
                re.compile(r'か[。\s]*$'),               # 「か」で終わる
                re.compile(r'(だろう|でしょう)か'),      # 「だろうか」「でしょうか」
            ],
            'imperative': [
                re.compile(r'(なさい|ください|てくれ|ておくれ|てちょうだい)[。!!]*$'),
                re.compile(r'(しろ|せよ|するな)[。!!]*$'),
            ],
            'conditional': [
                re.compile(r'(なら|ならば|たら|れば|ば)[、。]'),
                re.compile(r'(としたら|とすれば|であれば)[、。]'),
                re.compile(r'(ても|でも|といえども)[、。]'),
            ],
            'volition': [
                re.compile(r'(たい|たく|たがる)[。、!!]'),
                re.compile(r'(よう|まい)[。!!]*$'),
                re.compile(r'(だろう|でしょう)[。!!]*$'),
            ],
        }
        
        # 丁寧度を判定する正規表現
        self.politeness_patterns = {
            'polite': [
                # です・ますの各活用形(文末または読点の前)
                re.compile(r'(です|ですか|でした|でしょう)[。!?!?、]*$'),
                re.compile(r'(ます|ますか|ました|ません|ませんか|ましょう)[。!?!?、]*$'),
                re.compile(r'(ございます|ございました)[。!?!?、]*$'),
            ],
            'plain': [
                # だ・であるの各活用形(だけど、などを除外)
                re.compile(r'(?<![んだけそ])(だ|だった|だろう)[。!?!?]*$'),
                re.compile(r'(である|であった|であろう)[。!?!?]*$'),
                re.compile(r'(た|ない|なかった)[。!?!?]$'),
            ],
        }
        
        # 文の複雑度を判定する正規表現
        self.complexity_patterns = {
            'enumeration': [
                re.compile(r'、.+、.+'),  # 読点が2つ以上(列挙の可能性)
            ],
            'complex': [
                re.compile(r''),  # 読点あり(単純な接続)
                re.compile(r'(ので|のに|から|けれど|けれども|が、|し、|て、|で、)'),
            ],
        }
    
    def classify_function(self, text: str) -> str:
        """
        文の機能を分類
        
        Returns:
            'question': 質問文
            'imperative': 命令文・依頼文
            'conditional': 条件文
            'volition': 推量・意志表現
            'statement': 平叙文(デフォルト)
        """
        # 優先順位順にチェック
        for func_type, patterns in self.function_patterns.items():
            for pattern in patterns:
                if pattern.search(text):
                    return func_type
        
        return 'statement'  # デフォルト: 平叙文
    
    def classify_politeness(self, text: str) -> str:
        """
        丁寧度を分類
        
        Returns:
            'polite': 丁寧体(です・ます調)
            'plain': 普通体(だ・である調)
            'other': その他(体言止めなど)
        """
        for politeness_type, patterns in self.politeness_patterns.items():
            for pattern in patterns:
                if pattern.search(text):
                    return politeness_type
        
        return 'other'
    
    def classify_complexity(self, text: str) -> str:
        """
        文の複雑度を分類
        
        Returns:
            'enumeration': 列挙文(読点2つ以上)
            'complex': 複文(読点あり、または接続表現)
            'simple': 単文
        """
        for complexity_type, patterns in self.complexity_patterns.items():
            for pattern in patterns:
                if pattern.search(text):
                    return complexity_type
        
        return 'simple'
    
    def extract_features(self, text: str) -> SentenceFeatures:
        """
        文から全ての言語的特徴を抽出
        
        Args:
            text: 分析する日本語文
        
        Returns:
            SentenceFeatures: 抽出された特徴
        """
        return SentenceFeatures(
            text=text,
            function=self.classify_function(text),
            politeness=self.classify_politeness(text),
            complexity=self.classify_complexity(text),
            has_quote=bool(re.search(r'[「」『』]', text)),
            has_numeric=bool(re.search(r'[0-90-9]', text)),
            has_katakana=bool(re.search(r'[ァ-ヴー]{3,}', text)),  # 3文字以上のカタカナ
            char_count=len(text),
        )


# ============================================================================
# フィルタリング関数
# ============================================================================

def is_valid_text(text: str, min_len: int, max_len: int) -> Tuple[bool, str]:
    """
    テキストが品質基準を満たすかチェック
    
    Args:
        text: チェックする文字列
        min_len: 最小文字数
        max_len: 最大文字数
    
    Returns:
        (is_valid, reason): 有効性とその理由
    """
    # URL除外
    if re.search(r'https?://', text):
        return False, "contains_url"
    
    # メンション・ハッシュタグ除外
    if re.search(r'[@@##]', text):
        return False, "contains_mention_or_hashtag"
    
    # 絵文字・記号除外(より広範囲に)
    if re.search(r'[\u2600-\u27BF\U0001F300-\U0001F9FF]', text):
        return False, "contains_emoji"
    
    # HTML/XMLタグ除外
    if re.search(r'<[^>]+>', text):
        return False, "contains_html_tag"
    
    # 日本語文字が含まれない文を除外
    if not re.search(r'[ぁ-んァ-ヴー一-龥]', text):
        return False, "no_japanese_chars"
    
    # 同じ文字の繰り返し(スパム対策)
    if re.search(r'(.)\1{4,}', text):  # 同じ文字が5回以上
        return False, "repetitive_chars"
    
    # 文字数チェック
    char_count = len(text)
    if char_count < min_len:
        return False, f"too_short({char_count}<{min_len})"
    if char_count > max_len:
        return False, f"too_long({char_count}>{max_len})"
    
    return True, "ok"


# ============================================================================
# バランス抽出器
# ============================================================================

class BalancedSampler:
    """
    3軸分類に基づいてバランスの取れたサンプリングを行う
    
    戦略:
      1. 機能 × 丁寧度 × 複雑度の組み合わせでバケット作成
      2. 各バケットから均等に抽出(quota方式)
      3. 不足分はプールからランダム補充
    """
    
    def __init__(self, target_n: int, seed: int = 42):
        """
        Args:
            target_n: 抽出する文の総数
            seed: 乱数シード(再現性確保)
        """
        self.target_n = target_n
        self.seed = seed
        random.seed(seed)
        
        self.buckets: Dict[Tuple[str, str, str], List[SentenceFeatures]] = defaultdict(list)
    
    def add_sentence(self, features: SentenceFeatures):
        """バケットに文を追加"""
        key = features.get_bucket_key()
        self.buckets[key].append(features)
    
    def sample(self, verbose: bool = False) -> List[SentenceFeatures]:
        """
        バランスの取れたサンプリングを実行
        
        Returns:
            抽出された文のリスト
        """
        if not self.buckets:
            return []
        
        # 各バケットから抽出する数(quota)を計算
        num_buckets = len(self.buckets)
        quota = self.target_n // num_buckets
        
        picked: List[SentenceFeatures] = []
        pool: List[SentenceFeatures] = []
        
        if verbose:
            print(f"\n🎲 Sampling strategy:", file=sys.stderr)
            print(f"   Total buckets: {num_buckets}", file=sys.stderr)
            print(f"   Quota per bucket: {quota}", file=sys.stderr)
            print(f"   Target total: {self.target_n}", file=sys.stderr)
            print(f"\n📊 Sampling from each bucket:", file=sys.stderr)
        
        # 各バケットから均等に抽出
        for key in sorted(self.buckets.keys()):
            sentences = self.buckets[key]
            random.shuffle(sentences)
            
            # quota分を抽出
            taken = sentences[:quota]
            picked.extend(taken)
            
            # 残りはプールへ
            pool.extend(sentences[quota:])
            
            if verbose:
                func, pol, comp = key
                print(f"   {func:12s} × {pol:6s} × {comp:11s}: "
                      f"{len(sentences):5d} available → picked {len(taken):4d}",
                      file=sys.stderr)
        
        # 不足分をプールからランダム補充
        if len(picked) < self.target_n:
            random.shuffle(pool)
            need = self.target_n - len(picked)
            picked.extend(pool[:need])
            
            if verbose:
                print(f"\n🔄 Filled {need} more from pool (total: {len(picked)})",
                      file=sys.stderr)
        
        # 最終的にシャッフルして必要数だけ返す
        random.shuffle(picked)
        return picked[:self.target_n]


# ============================================================================
# 統計レポート生成
# ============================================================================

def generate_statistics_report(
    picked: List[SentenceFeatures],
    stats: Counter,
    verbose: bool = False
) -> str:
    """
    抽出結果の詳細な統計レポートを生成
    
    Args:
        picked: 抽出された文のリスト
        stats: 処理中の統計情報
        verbose: 詳細表示フラグ
    
    Returns:
        統計レポートの文字列
    """
    lines = []
    
    # 基本統計
    lines.append("\n" + "=" * 70)
    lines.append("📈 FINAL STATISTICS")
    lines.append("=" * 70)
    
    # 文字数統計
    lengths = [f.char_count for f in picked]
    lines.append(f"\n📏 Character count statistics:")
    lines.append(f"   Min:    {min(lengths):3d} chars")
    lines.append(f"   Max:    {max(lengths):3d} chars")
    lines.append(f"   Mean:   {statistics.mean(lengths):5.1f} chars")
    lines.append(f"   Median: {statistics.median(lengths):5.1f} chars")
    
    # 主要分類軸の分布
    lines.append(f"\n📊 Distribution by main axes:")
    
    # 機能別
    func_counter = Counter(f.function for f in picked)
    lines.append(f"\n   🎯 Function:")
    for func in ['question', 'statement', 'imperative', 'conditional', 'volition']:
        count = func_counter[func]
        pct = 100 * count / len(picked)
        lines.append(f"      {func:12s}: {count:5d} ({pct:5.1f}%)")
    
    # 丁寧度別
    pol_counter = Counter(f.politeness for f in picked)
    lines.append(f"\n   🙇 Politeness:")
    for pol in ['polite', 'plain', 'other']:
        count = pol_counter[pol]
        pct = 100 * count / len(picked)
        lines.append(f"      {pol:6s}: {count:5d} ({pct:5.1f}%)")
    
    # 複雑度別
    comp_counter = Counter(f.complexity for f in picked)
    lines.append(f"\n   🔗 Complexity:")
    for comp in ['simple', 'complex', 'enumeration']:
        count = comp_counter[comp]
        pct = 100 * count / len(picked)
        lines.append(f"      {comp:11s}: {count:5d} ({pct:5.1f}%)")
    
    # 補助的特徴
    lines.append(f"\n   📌 Auxiliary features:")
    quote_count = sum(1 for f in picked if f.has_quote)
    numeric_count = sum(1 for f in picked if f.has_numeric)
    katakana_count = sum(1 for f in picked if f.has_katakana)
    
    lines.append(f"      Has quote:    {quote_count:5d} ({100*quote_count/len(picked):5.1f}%)")
    lines.append(f"      Has numeric:  {numeric_count:5d} ({100*numeric_count/len(picked):5.1f}%)")
    lines.append(f"      Has katakana: {katakana_count:5d} ({100*katakana_count/len(picked):5.1f}%)")
    
    # 詳細な組み合わせ統計(verbose時のみ)
    if verbose:
        lines.append(f"\n   🔍 Detailed combination statistics (Top 10):")
        combo_counter = Counter(f.get_bucket_key() for f in picked)
        for (func, pol, comp), count in combo_counter.most_common(10):
            pct = 100 * count / len(picked)
            lines.append(f"      {func:12s} × {pol:6s} × {comp:11s}: "
                        f"{count:4d} ({pct:4.1f}%)")
    
    lines.append("\n" + "=" * 70)
    
    return "\n".join(lines)


# ============================================================================
# メイン処理
# ============================================================================

def parse_arguments():
    """コマンドライン引数をパース"""
    parser = argparse.ArgumentParser(
        description="Tatoebaから言語学的にバランスの取れた日本語文を抽出",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用例:
  # 基本実行(デフォルト設定)
  python extract_ja_perfect.py --verbose
  
  # 短文データセット(チャットボット用)
  python extract_ja_perfect.py --n 5000 --min_len 10 --max_len 30 --out ja_short.csv
  
  # 長文データセット(要約タスク用)
  python extract_ja_perfect.py --n 3000 --min_len 50 --max_len 100 --out ja_long.csv
        """
    )
    
    parser.add_argument(
        "--src",
        default="sentences.csv",
        help="入力CSVファイル(Tatoebaのsentences.csv)"
    )
    parser.add_argument(
        "--out",
        default="ja_balanced_10000.csv",
        help="出力CSVファイル"
    )
    parser.add_argument(
        "--n",
        type=int,
        default=10000,
        help="抽出する文の数(デフォルト: 10000)"
    )
    parser.add_argument(
        "--min_len",
        type=int,
        default=20,
        help="最短文字数(デフォルト: 20)"
    )
    parser.add_argument(
        "--max_len",
        type=int,
        default=50,
        help="最長文字数(デフォルト: 50)"
    )
    parser.add_argument(
        "--seed",
        type=int,
        default=42,
        help="乱数シード(再現性確保、デフォルト: 42)"
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="詳細なログを出力"
    )
    
    return parser.parse_args()


def main():
    """メイン処理"""
    args = parse_arguments()
    
    # 入力ファイルの存在確認
    input_path = Path(args.src)
    if not input_path.exists():
        print(f"❌ Error: 入力ファイルが見つかりません: {args.src}", file=sys.stderr)
        print(f"\n💡 Hint: 以下のコマンドでファイルをダウンロードしてください:", file=sys.stderr)
        print(f"   wget https://downloads.tatoeba.org/exports/sentences.tar.bz2", file=sys.stderr)
        print(f"   tar -xjf sentences.tar.bz2", file=sys.stderr)
        sys.exit(1)
    
    # ヘッダー表示
    print("=" * 70, file=sys.stderr)
    print("🌸 Tatoeba Japanese Sentence Extractor [Perfect Edition]", file=sys.stderr)
    print("=" * 70, file=sys.stderr)
    print(f"📖 Input:  {args.src}", file=sys.stderr)
    print(f"📝 Output: {args.out}", file=sys.stderr)
    print(f"🎯 Target: {args.n:,} sentences", file=sys.stderr)
    print(f"📏 Length: {args.min_len}-{args.max_len} chars", file=sys.stderr)
    print(f"🌱 Seed:   {args.seed}", file=sys.stderr)
    print("=" * 70, file=sys.stderr)
    
    # 分類器とサンプラーの初期化
    classifier = JapaneseSentenceClassifier()
    sampler = BalancedSampler(target_n=args.n, seed=args.seed)
    
    # 統計カウンター
    stats = Counter()
    seen_texts = set()
    line_count = 0
    
    # データ読み込みと処理
    print(f"\n📚 Reading and classifying sentences...", file=sys.stderr)
    
    try:
        with open(args.src, 'r', encoding='utf-8', newline='') as f:
            reader = csv.reader(f, delimiter='\t')
            
            for row in reader:
                line_count += 1
                
                # プログレス表示
                if args.verbose and line_count % 100000 == 0:
                    print(f"   ... processed {line_count:,} lines", file=sys.stderr)
                
                # 行の形式チェック
                if len(row) < 3:
                    stats['malformed'] += 1
                    continue
                
                sentence_id, lang, text = row[0], row[1], row[2].strip()
                
                # 日本語のみ対象
                if lang != 'jpn':
                    continue
                
                stats['total_jpn'] += 1
                
                # 重複除外
                if text in seen_texts:
                    stats['duplicate'] += 1
                    continue
                
                # 品質チェック
                is_valid, reason = is_valid_text(text, args.min_len, args.max_len)
                if not is_valid:
                    stats[f'filtered_{reason}'] += 1
                    continue
                
                # 特徴抽出してサンプラーに追加
                features = classifier.extract_features(text)
                sampler.add_sentence(features)
                
                seen_texts.add(text)
                stats['accepted'] += 1
        
        print(f"✅ Processed {line_count:,} lines", file=sys.stderr)
        
    except Exception as e:
        print(f"❌ Error reading file: {e}", file=sys.stderr)
        sys.exit(1)
    
    # 処理統計の表示
    print(f"\n📊 Processing statistics:", file=sys.stderr)
    print(f"   Total lines:      {line_count:,}", file=sys.stderr)
    print(f"   Japanese:         {stats['total_jpn']:,}", file=sys.stderr)
    print(f"   Accepted:         {stats['accepted']:,}", file=sys.stderr)
    print(f"   Filtered:         {sum(v for k, v in stats.items() if k.startswith('filtered_')):,}", file=sys.stderr)
    print(f"   Duplicates:       {stats['duplicate']:,}", file=sys.stderr)
    
    if args.verbose:
        print(f"\n   Detailed filter reasons:", file=sys.stderr)
        for key in sorted(k for k in stats.keys() if k.startswith('filtered_')):
            print(f"      {key:30s}: {stats[key]:,}", file=sys.stderr)
    
    # 受け入れられた文が不足している場合
    if stats['accepted'] < args.n:
        print(f"\n⚠️  Warning: 条件に合う文が {stats['accepted']:,} 件しか見つかりませんでした", file=sys.stderr)
        print(f"   目標の {args.n:,} 件に対して不足しています", file=sys.stderr)
        print(f"\n💡 解決策:", file=sys.stderr)
        print(f"   1. --n の値を減らす (例: --n {stats['accepted'] // 2})", file=sys.stderr)
        print(f"   2. 文字数範囲を広げる (例: --min_len 15 --max_len 60)", file=sys.stderr)
        
        # それでも続行可能なら続行
        if stats['accepted'] == 0:
            print(f"\n❌ Error: 抽出可能な文が0件です。処理を中止します。", file=sys.stderr)
            sys.exit(1)
    
    # バランス抽出の実行
    picked = sampler.sample(verbose=args.verbose)
    
    if not picked:
        print(f"❌ Error: サンプリングに失敗しました", file=sys.stderr)
        sys.exit(1)
    
    # 出力ファイルへの書き込み
    try:
        output_path = Path(args.out)
        with open(output_path, 'w', encoding='utf-8', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(['ja'])  # ヘッダー
            
            for features in picked:
                writer.writerow([features.text])
        
        print(f"\n✅ Successfully wrote {len(picked):,} sentences to: {args.out}",
              file=sys.stderr)
        
    except Exception as e:
        print(f"❌ Error writing output file: {e}", file=sys.stderr)
        sys.exit(1)
    
    # 統計レポートの生成と表示
    report = generate_statistics_report(picked, stats, verbose=args.verbose)
    print(report, file=sys.stderr)
    
    print("\n🎉 Complete! Enjoy your perfectly balanced dataset!", file=sys.stderr)
    print("=" * 70, file=sys.stderr)


if __name__ == "__main__":
    main()
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?