【エラー解析】「IndexError: list index out of range」が暴露する自作Datasetクラスの落とし穴

プログラミング

独自の画像ファイルやテキストセットをスクレイピング等でかき集め、オリジナルのAIモデルを学習させよう!と DataLoader を回し始めた瞬間。
最初の数バッチは「Loss: 1.25… Loss: 1.10…」と順調に学習できていたのに、1時間後に見に来たら途中で突如クラッシュしているという最悪のエラー。

IndexError: list index out of range in dataset __getitem__

「えっ?リストの範囲外にアクセスしたって?最初の100枚は上手く読めていたのになぜ突然インデックスエラー?」
このエラーの中心には、PyTorchのデータ読み込みパイプライン(DatasetとDataLoader)の構造的な取り決めと、現実世界に存在する「汚いデータセット(ノイズ)」という厄介な実情が横たわっています。本記事で、絶対に学習が止まらない「防弾仕様のDataLoader」の作り方をマスターしましょう。

1. PyTorch Datasetの厳重な掟(コントラクト)

PyTorchの torch.utils.data.Dataset クラスを自作(オーバーライド)してオリジナルデータを読み込ませる際、必ず定義しなければならない「2つの魔法のメソッド」があります。

  • __len__(self):
    データセットの総データ数(枚数など)を返す。モデルに「全部で何個データがあるか」を教えます。
  • __getitem__(self, idx):
    0 から len - 1 までのインデックス(番号) idx を受け取り、その番号のデータ(画像とラベルのペア等)をディスクから読み込んで返す。

ここからが重要です。PyTorchの DataLoader は、__len__ が返した数値(例えば1000)を「絶対に裏切られない神の数字」として無条件に信じ込みます。
そして裏側で別スレッド(マルチプロセッシング機能:num_workers)をバリバリ立ち上げて、idx = 0 から idx = 999 までの値を容赦なくランダムに __getitem__ に投げ込みまくります。

つまり、「画像は本当は990枚しかないのに、プログラムのミスや勘違いで __len__ が1000を返してしまっていた」場合や、フォルダ内部の構成にズレが生じていた場合、DataLoaderは当然のように「存在しない995枚目の画像をよこせ!」とアクセスし、配列外参照(IndexError)で大爆発して学習プロセス全体が死ぬわけです。

# 【バグを生む非常に危険なDatasetの例】
import os
from torch.utils.data import Dataset
from PIL import Image
class MyDataset(Dataset):
    def __init__(self, image_dir):
        # 危険!単純にフォルダ内の全ファイル名をごっそり取得しているだけ
        self.image_files = os.listdir(image_dir)
        
    def __len__(self):
        # もし隠しファイルがあれば、実際の画像枚数とズレてしまう!
        return len(self.image_files)
        
    def __getitem__(self, idx):
        # idx が実際の画像枚数を超えてアクセスされたり、
        # あるいは .DS_Store なんかを画像として読み込もうとしてエラーになる!
        img_name = self.image_files[idx]
        image = Image.open(img_name).convert("RGB")
        return image

2. 「汚いデータ」と防弾仕様(Robust)のDataLoader設計

現実のスクレイピングデータや現場のログファイルは、手つかずのジャングルと同じです。「ダウンロード途中で破損してPILで開けない画像」「0バイトの空ファイル」「Macが勝手に作った .DS_Store やWindowsの Thumbs.db などのシステム隠しファイル」が必ず、100%混入しています。

これらが __getitem__ にヒットした場合、Image.open() は例外(Exception)を吐き、それが結果的にリスト解析を狂わせたりDataLoaderのマルチプロセッシングを急停止(クラッシュ)させたりします。「一晩かけて回した学習が、残り10%のところで壊れた1枚の画像のせいで全部パーになった」なんて悲劇は日常茶飯事です。

堅牢なシステムを構築するためには、初期化(__init__)の段階で全データのバリデーション(健全性チェック)を行い、不正なデータをあらかじめリストから完全に除外しておくという前処理プロセスが必須となります。

class RobustDataset(Dataset):
    def __init__(self, image_dir):
        # 1. 拡張子レベルでまずはフィルタリング(隠しファイル等を除外)
        valid_extensions = {".jpg", ".jpeg", ".png"}
        all_files = [os.path.join(image_dir, f) for f in os.listdir(image_dir)]
        
        self.valid_images = []
        
        # 2. 実際に開けるか、破損していないかの健全性チェック(初期化時に一度だけ実行!)
        print("データセットの整合性をチェック中...")
        for f in all_files:
            if os.path.splitext(f)[-1].lower() in valid_extensions:
                try:
                    # img.verify() はヘッダ情報だけで壊れていないかチェックするため非常に高速
                    with Image.open(f) as img:
                        img.verify()
                    self.valid_images.append(f)
                except Exception:
                    # 破損したデータは無視し、真に使える画像だけをリストに残す
                    print(f"破損した画像をリストから除外しました: {f}")
                    
    def __len__(self):
        # これでDataLoaderは「安全が保証された完璧な画像枚数」だけを参照する
        return len(self.valid_images) 
        
    def __getitem__(self, idx):
        # ここでは100%安全に画像が開けることが保証されている!
        img_path = self.valid_images[idx]
        image = Image.open(img_path).convert("RGB")
        return image

まとめ:エラーは「AIはアルゴリズムではなくデータで動く」ことの証明

ニューラルネットワークのアーキテクチャ設計がどれほど優秀で画期的だったとしても、パイプラインの入口に流し込む「データ(血液)」が汚染されていればシステムはあっけなく死にます。

インデックスエラーや予期せぬIOファイルエラーは、我々エンジニアに「データの泥臭い前処理と監視戦略(Data Governance)」の重要性を痛感させてくれます。
高度なAIアルゴリズムをチューニングする前に、まずはPythonによるロバスト(堅牢)なファイル処理・データクレンジング技術を極めることが、実務において最強で最速の近道となります。

コメント

タイトルとURLをコピーしました