Featured image of post 機械学習におけるデータとデータソースの再考

機械学習におけるデータとデータソースの再考

目次

背景

  • 機械学習におけるデータセットの分割の再考で、Train・Val・Diag・Challenge・Testという5つの役割にデータを整理した
  • 今回は、それぞれの役割を担うデータを実際にどう作るか、という作成方法の観点から再整理する
  • 特に、どれを自動生成してよく、どれを手動で作るべきか、そしてその理由を明確にする
  • ここではデータそのもの(データソース・収集方法・品質)を扱う。タクソノミー・クラス設計・ラベルの信頼度といった、ラベル・クラス側の設計は機械学習におけるクラスとラベルの再考で扱う

前回のおさらい: 5つの役割

Dataset主目的開発者が見るか
TrainParameter LearningYes
ValModel SelectionYes
DiagError AnalysisYes
ChallengeFailure-mode / Capability EvaluationYes
TestFinal Generalization EstimateNo

Train・Valの作り方

自動生成してよい

  • Trainは自動生成(合成データ生成など)で作ってよい
  • Trainは「モデルに学習させたい範囲=スコープ」を定義するデータであり、生成プロセス自体がそのままスコープの定義になる
  • 生成プロセスをコントロールできるからこそ、狙った分布・狙ったカバレッジのデータを効率よく用意できる

データソースの多様性

「自動生成」といっても、その元になるデータをどこから集めるかには、いくつか毛色の異なるアプローチがある。

  • 辞書などの硬いデータソース: 辞書・法令・規格表のような、構造が明確で信頼性の高いデータソース。カバレッジや多様性には限界があるが、ラベルの正確さは高い
  • 関連するデータソース(間接的な転用): 本来別の目的で作られたデータが、副産物として目的のラベル・構造を含んでいることがある。本来の作成目的とは別の切り口でデータを転用する発想
  • ネットからのキュレーション: Web上の大量のテキスト・画像などから、パターンマッチングやフィルタリングで目的のデータを抽出する。スケールは出しやすい一方、ノイズが多く、前述のデータクレンジングが特に重要になる

これらは互いに独立したソースになりやすいという利点もある。複数のアプローチでTrainを集めておくと、データの多様性が増すだけでなく、前述の「トレイン系と診断系で元データソースを分ける」という要求にも応えやすくなる。

具体的な収集アイデアの実例は、G2Pライブラリを作る時に調査した先行文献などのメモのデータ収集のアイデアで詳しくまとめている。

収集時の実践

データを集める段階そのものにも、気をつけるべき実践がいくつかある。

  • PositiveだけでなくNegativeも意図して集める: Negativeの収集を疎かにすると、実運用でどんな入力が来るか分からないままモデルを評価することになる。特にhard negativeやOODになりうる入力は、Positiveと同じくらい丁寧に設計して集めるべき
    • Positiveがa・b・cという特徴を持っていて本当はaだけを学習させたい場合、b・cは持つがaは持たないNegativeがないと、モデルが間違った特徴を学習してしまう(例: 音声からある単語を学習させたかったのに、Negativeが不足していたため単語ではなく話者の喋り方を学習してしまった、というケース)
  • 中間データも保存する: 最終的に使う加工済みデータだけでなく、加工前の中間データも保存しておく。後から加工方法自体を見直したくなった時に、収集からやり直さずに済む
  • ダブルチェックをする: ラベル付けや収集作業は、1人だけでなく複数人でダブルチェックする。特にAIやクラウドソーシングで生成・収集したデータは、人の目でのダブルチェックが必須

ホールドアウトでTrain/Valに分ける

  • 生成したデータプールを、ホールドアウトでTrainとValに分割する
  • つまりTrainとValは、どちらも同じ生成プロセスに由来する「スコープ内(in-scope)」のデータという位置づけになる
  • Valはモデル選択に使うため、Trainと同じ分布から取れていれば十分で、独立性の要求はDiag・Testほど厳しくない
  • 分割の際は、元データのクラス比率(経験的な事前確率$P(Y)$)をなるべく維持したstratified splitで行う
  • 例えば元データがA:70%, B:20%, C:10%なら、Train/Val/Testもそれぞれ概ね70/20/10になるように分割する

Diagの作り方

自動生成を避け、手動で作る

  • Diagは基本的に自動生成を避け、人手で作ることが望ましい
  • 理由は、統計的な優位性(有意性)を主張するために、サンプルの独立性が必要だから
  • Trainと同じ生成プロセス(同じモデル・同じテンプレート・同じ乱数源など)でDiagも自動生成すると、TrainとDiagのサンプルが統計的に独立でなくなる
  • 独立性が崩れると、Diagで良い結果が出たとしても、それが本当の汎化性能なのか、単に生成プロセスの癖への過学習を反映しているだけなのかを区別できなくなる
  • 少ないサンプルサイズでも意味のある信頼区間を持たせるには、まずサンプル同士の独立性という前提を満たしておく必要がある
$$ \hat{p} = \frac{x}{n} $$
  • 上記のような単純な比率推定であっても、信頼区間の妥当性は$n$個のサンプルが独立であることを前提にしている
  • 独立性が崩れていると、見かけの$n$より実質的な情報量は少なく、信頼区間は実態より狭く(楽観的に)出てしまう

スコープ内・スコープ外・OODを含める

  • Diagには、in-scopeのデータだけでなく、out-of-scopeのデータ、そしてOOD(Out-of-Distribution)のデータも意図的に含める
  • 理由は、モデルの能力の幅を測れるようにするため
  • in-scopeのデータだけでは「スコープ内でどれだけうまくいっているか」しか分からず、スコープの境界がどこにあり、境界を越えるとどう崩れるかが見えない
  • スコープ内・スコープ外・OODを横断的に含めることで、Diag1つで「境界の内側の性能」と「境界そのものの位置」の両方を診断できる

Testの作り方

外部データソースをなるべく使う

  • 評価データセット(Test)は、基本的に外部のデータソースをなるべく利用する
  • 理由は、自分でデータを生成すると、事前確率(prior)が変わってしまうから
  • 自分の生成プロセスには、無意識のうちにクラス比率・パターンの偏りが入り込みやすく、その偏った事前確率のもとで測った性能は、現実の事前確率のもとでの性能とズレる
  • 外部のデータソースを使うことで、こうした自己言及的な偏りを避け、より現実の事前確率に近い状態で評価できる
  • さらに一歩進めて、自前データを一切混ぜず、出来合いの公開データセットのみ100%でテストセットを作るという手もある。生成過程が完全に独立したデータで評価できるため、自前データの収集過程に共通する癖への過学習を検知しやすくなる
    • これは精度を追求する本番用ではなく、あくまで評価のための物差しとして使う位置づけ
    • 他人の作ったデータセットや手法を再現しようとすると、特徴量の計算方法など自分では気づかなかった変えられる点が色々見つかるため、学びや改善幅も大きい

エラーアナリシス禁止

  • 評価データセットに対するエラーアナリシスは禁止
  • 集計されたスコア以外の目的で、中身を見ること自体も禁止
  • これは前回の記事で扱った「Testを覗いた瞬間、測っている量が無条件の汎化性能から、そのTestに条件付けられた量にすり替わる」という問題への対策そのもの

データソースの分離

トレイン系と診断系は元データソースを分ける

  • データセットを作る際、元となる生データソース自体を、トレインデータ系(Train・Val)と診断データセット(Diag)とで必ず分ける
  • 同じ生データプールからランダムに分割するだけでは、独立性の担保として不十分になりうる
  • 完全に別のデータソース(別の収集経路、別の収集時期、別の生成方法など)を用意することで、より確実な独立性を担保する
  • Testについても同様に、Train・Valはもちろん、Diagとも別のソースであることが望ましい

データの量

サンプル数そのものが必要

  • seedを固定して実験がべき等に実行でき、2回同じ実験をして全く同じ結果が返ってきたとしても、サンプルサイズは別問題として必要
  • サンプル数が極端に少ないと、実験の比較としては再現できても、実際にはドメインシフトが起きている可能性を検知できない
  • 評価データは結局は注ぎ足し継ぎ足しで増やしていくものなので、最終的には数そのものが重要になる
  • 経験則として、1クラスあたりのサンプルサイズが50以下のholdoutでは、統計的に優位な数値が出ないことが多い
  • 学習時にべき等であっても、評価時にべき等とは限らないため、評価方法自体にも注意が必要

相対的な量にも注意する

  • クラス間の相対的なサンプル量にも注意が必要
  • 例えば、AクラスとBクラスがある評価データで、Bクラスのデータ(holdoutも含む)だけを増やすと、全体の指標がBクラス過多の影響を受けてしまう
  • ミクロ指標(全体の精度)はBクラスに引っ張られ、平等な精度の指標ではなくなる
  • Aクラスは相対的に量が少なくなり、学習側の注力もBクラスに大きく振られている可能性が出てくる
  • つまり、もともと学習できていたAクラスの特徴を、モデル容量不足のために上書きしてしまっている可能性がある
  • この場合、モグラたたき的にデータを直すのではなく、モデル容量の不足自体を疑うべき
  • データ量を増やす際は、モデルのパラメータも合わせて増やす必要がないかを検討する
  • 精度を報告する際も、1教科で90点なのか10教科で90点なのかで意味が全く違うように、分母(サンプルサイズ・クラス数)を必ず加味する

データ品質QA

ここまでの役割分担・独立性の設計を正しく行っても、データそのものにバグがあれば意味がない。何よりも大切なのは、データの品質QAである。

最小構成でのミニテスト

  • 数百万パターンにもなるような大きな予測対象を扱う場合、いきなり全件で学習・評価するのではなく、まず1件だけを使ってデータ生成・学習・テストを一通り試すのも有効
  • パイプライン全体(データ生成→学習→推論)を最小構成で一度通してみて、勘所を掴むのが目的
  • 1件だけの学習すら成功しない(lossが下がりきらない)なら、パターン数を増やす前にやり方自体を疑うべき
  • 逆に1件で学習できるなら、そのまま件数を横展開していけばよい
  • 機械学習分野では一般に「overfit a single batch」と呼ばれる、学習パイプラインのsanity checkの定番手法

データビュアーを作る

  • seedデータのデータビュアーは作っておくとよい
  • 大量データの品質チェックを高速化するのが目的で、データが多くなるほどチェックの負担が大きくなるため、ビュアーの有無で作業効率が大きく変わる

そもそもデータセットの確認を先にする

  • ゴミを入れたらゴミが出るだけなので、モデル改善の前にまずデータそのものをチェックするべき
  • Trainデータが一部間違っていた結果、精度が出ていないだけという初歩的なミスもある
  • そもそも分母のデータは正常か、評価のサンプルサイズは足りているか、エラーの例は妥当かをチェックしたうえで、モデルのパラメータを増やすなどの改善フローに入るべき
  • 特にAIによって生成されたデータは、確実にチェックが必要
  • 可視化した結果、現実的に高精度な分類が厳しいと分かることもある。その場合は、モデルを大きくする以外に、前提の制約自体を変えて境界線がはっきり出るように要件とデータセットを見直すという選択肢もある

データのバグを発見するためのコード

  • 内部矛盾スキャン: 同じ入力に対して異なるラベルが記録されているケースを機械的に全件抽出し、その組だけをレビューする。ランダムサンプリングよりずっと高い効率で誤りを見つけられる
1
2
3
4
5
6
7
8
9
def find_label_conflicts(dataset):
    seen = {}
    conflicts = []
    for sample in dataset:
        key = sample["input"]
        if key in seen and seen[key] != sample["label"]:
            conflicts.append((key, seen[key], sample["label"]))
        seen[key] = sample["label"]
    return conflicts
  • スキーマ検証: 必須フィールドの欠落・型の不一致・値域の逸脱などを機械的にチェックする
  • 統計的な外れ値検出: 文字数・トークン数・クラス比率などの分布を可視化し、極端な外れ値を確認する

データパイプラインのテストコード

  • データを生成・変換するパイプライン自体にもバグが入り込むため、通常のソフトウェアと同様にテストが必要
  • 過去に見つかったデータバグは、回帰テストとして固定化しておく
1
2
3
4
def test_known_bug_regression():
    # 過去に発見された特定の変換バグの再発を防ぐ
    result = normalize(raw_input)
    assert result == expected_output
  • 盲検サンプリングとWilson信頼区間を使い、パイプライン適用後のデータのエラー率を定期的に監視する
$$ \hat{p} = \frac{x}{n}, \qquad \text{95\%CIはサンプル数}n\text{が小さいほど広くなる} $$

重複除去(Dedupe)

  • データセットは基本的に複数のデータソースから集めることが多く、その過程で同じサンプルが重複して混入する可能性がある
  • 重複データは単なる水増しになるだけでなく、見かけ上のサンプルサイズを実態以上に大きく見せ、独立性の前提も崩してしまう
  • 重複チェックには、少なくとも以下の軸がある
    • 元データ・データソース間の重複: 複数の生データソースをマージする際、同じサンプルが重複して入り込むのを防ぐ
    • 単一データセット内の重複: 同じサンプルがTrainなどの中で複数回カウントされていないか
    • データセット間の重複: 前述の「元データソースを分ける」という原則を設計していても、実際にTrain・Val・Diag・Test間で重複が混入していないかを機械的に確認する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def find_duplicates(dataset):
    seen = {}
    duplicates = []
    for sample in dataset:
        key = hash(sample["input"])
        if key in seen:
            duplicates.append((seen[key], sample["id"]))
        else:
            seen[key] = sample["id"]
    return duplicates
  • 完全一致だけでなく、表記ゆれなどによる「ほぼ重複」も見逃しやすいため、正規化してから比較する、あるいは類似度ベースの重複検出も併用するとよい

データクレンジング

  • 実際に遭遇したデータクレンジングの具体的な問題・方法論は、別記事フリガナデータをクレンジングする時に直した項目のメモで詳しくまとめている
  • 大きく分けると、文字・表記レベル、読み表記レベル、意味・文脈レベル、分割・アライメントレベル、ソース・アノテーション品質レベル、分布・統計レベル、実装・インフラレベルという複数のレベルで問題が起こりうる
  • 個別のバグ分類以上に汎用性が高いのは、盲検サンプリング・独立クロスチェック・ノイズフロアの計測といった、データを直す際の方法論そのもの

まとめ

  • Train・Valは自動生成でよく、同じ生成プロセスからstratified splitでホールドアウトして分割する、スコープ内のデータ
  • データソースには、辞書などの硬いデータソース・関連するデータソースの転用・ネットからのキュレーションという毛色の異なるアプローチがあり、複数使うことで多様性とソース分離の両方に貢献する
  • Diagは自動生成を避け、独立性を保つために手動で作る。さらにin-scope・out-of-scope・OODを横断的に含めることで、能力の境界そのものを診断できるようにする
  • Testはなるべく外部データソースを使い、事前確率のズレを避ける。エラーアナリシスや中身の閲覧は禁止
  • 元データソース自体を、トレイン系と診断系で分けておくことが、統計的な独立性を担保する土台になる
  • サンプル数は絶対量(1クラスあたり最低限の目安)と相対量(クラス間バランス)の両方に注意し、精度を報告する際は分母を必ず加味する
  • 役割分担・独立性を正しく設計しても、データ自体にバグがあれば意味が無い。最小構成でのミニテストやデータの確認から始め、バグ発見コード・パイプラインのテストコード・重複除去・データクレンジングという品質QAを積み重ねることが何より大切
  • 複数のデータソースから集める以上、重複データの混入は避けられないため、データセット内・データセット間の両方で重複チェックが必要
  • タクソノミー・クラス設計・Grade・ラベルの信頼度(Gold/Silver)といった、ラベル・クラス側の設計は機械学習におけるクラスとラベルの再考で扱う

参考文献

Built with Hugo
テーマ Stack は Jimmy によって設計されています。