エッジAI

データセット

機械学習の学習に使うデータの集合。

概要

データセット(Dataset)とは、機械学習・ディープラーニングモデルの学習・検証・テストに使用するデータの集合です。機械学習において「データはモデルの原料」であり、良質なデータセットなくして高精度なモデルは実現できません。「Garbage in, garbage out(粗悪な入力からは粗悪な出力しか生まれない)」という原則がAI開発では特に重要です。

データセットは通常、以下の三つに分割して使用します。

分割割合(目安)用途
訓練セット(Train)60〜80%モデルのパラメータ学習に使用
検証セット(Validation)10〜20%ハイパーパラメータ調整・過学習確認
テストセット(Test)10〜20%最終的な性能評価(学習には使わない)

組み込み・エッジAI向けのデータセット構築では、デプロイ環境(工場内・屋外・車内など)に即したデータ収集が特に重要で、ドメインシフト(学習環境と実環境の乖離)が性能低下の主要因となります。

歴史・背景

機械学習用データセットは学術研究の発展とともに整備されてきました。

代表的なデータセットと影響:

データセット規模影響
1998年MNIST(手書き数字)7万枚画像認識の入門標準
2009年ImageNet1400万枚、1000クラスディープラーニング革命の基盤
2014年COCO33万枚、80クラス物体検出・セグメンテーションの標準
2017年Open Images900万枚、600クラス大規模ラベル付き画像
2017年AudioSet200万クリップ音声分類の基盤
2019年MVTec AD5354枚(産業異常検知)工業外観検査の標準
2020年Common Voice多言語音声オープン音声認識
2021年LAION-400M4億画像-テキストペア生成AIの基盤

技術仕様

データセットの種類(タスク別)

データセットの分類

画像系
├── 画像分類: ImageNet, CIFAR-10/100, Flowers102
├── 物体検出: COCO, Pascal VOC, Open Images
├── セグメンテーション: ADE20K, Cityscapes
└── 異常検知: MVTec AD, VisA, BTAD

音声系
├── 音声認識: LibriSpeech, Common Voice, FLEURS
├── ウェイクワード: Google Speech Commands
└── 環境音: ESC-50, AudioSet

時系列・センサー系
├── 振動/故障: CWRU Bearing, FEMTO Bearing
├── 活動認識: UCI HAR, PAMAP2
└── ECG/医療: MIT-BIH Arrhythmia

自然言語処理
├── 分類: SST-2, AG News
├── 質疑応答: SQuAD, Natural Questions
└── 翻訳: WMT, FLORES

データセットの品質指標

import numpy as np
from collections import Counter

def analyze_dataset_quality(labels, data):
    """データセットの品質を分析"""
    
    # 1. クラス分布の確認(クラス不均衡)
    class_counts = Counter(labels)
    total = len(labels)
    print("=== クラス分布 ===")
    for cls, count in sorted(class_counts.items()):
        print(f"  クラス{cls}: {count}枚 ({count/total*100:.1f}%)")
    
    # 不均衡比の計算
    max_count = max(class_counts.values())
    min_count = min(class_counts.values())
    imbalance_ratio = max_count / min_count
    print(f"不均衡比: {imbalance_ratio:.1f}x")
    if imbalance_ratio > 10:
        print("警告: クラス不均衡が大きい。"
              "オーバーサンプリングや重み付けを検討してください")
    
    # 2. データ統計
    if data is not None:
        print("\n=== データ統計 ===")
        print(f"  平均: {np.mean(data):.3f}")
        print(f"  標準偏差: {np.std(data):.3f}")
        print(f"  最小値: {np.min(data):.3f}")
        print(f"  最大値: {np.max(data):.3f}")
    
    return class_counts, imbalance_ratio

アノテーション形式

物体検出データセットのアノテーションには複数の形式があります。

# COCO形式(JSON)
{
    "images": [
        {"id": 1, "file_name": "img001.jpg", 
         "width": 640, "height": 480}
    ],
    "annotations": [
        {
            "id": 1,
            "image_id": 1,
            "category_id": 1,
            "bbox": [100, 50, 200, 150],  # [x, y, width, height]
            "area": 30000,
            "iscrowd": 0
        }
    ],
    "categories": [
        {"id": 1, "name": "person"},
        {"id": 2, "name": "car"}
    ]
}

# YOLO形式(テキスト、各行が1つのオブジェクト)
# <class_id> <x_center> <y_center> <width> <height>
# 全て正規化座標(0〜1.0)
# 0 0.512 0.324 0.312 0.425

# Pascal VOC形式(XML)
"""
<annotation>
  <filename>img001.jpg</filename>
  <size><width>640</width><height>480</height></size>
  <object>
    <name>person</name>
    <bndbox>
      <xmin>100</xmin><ymin>50</ymin>
      <xmax>300</xmax><ymax>200</ymax>
    </bndbox>
  </object>
</annotation>
"""

動作原理

データ拡張(Data Augmentation)

限られたデータから学習を効果的にするため、既存データを変換して擬似的にデータ量を増やします。

import albumentations as A
import cv2
import numpy as np

# 画像分類向けデータ拡張パイプライン
classification_transform = A.Compose([
    # 幾何学的変換
    A.RandomRotate90(p=0.5),
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.2),
    A.ShiftScaleRotate(
        shift_limit=0.1, scale_limit=0.2,
        rotate_limit=15, p=0.5
    ),
    # 色調変換
    A.RandomBrightnessContrast(
        brightness_limit=0.2, contrast_limit=0.2, p=0.4
    ),
    A.HueSaturationValue(
        hue_shift_limit=10, sat_shift_limit=30, p=0.3
    ),
    A.ColorJitter(p=0.3),
    # ノイズ・ぼかし
    A.GaussNoise(var_limit=(10, 50), p=0.2),
    A.MotionBlur(blur_limit=5, p=0.2),
    # 正規化
    A.Normalize(mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225]),
])

# 物体検出向けデータ拡張(バウンディングボックスも変換)
detection_transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.RandomBrightnessContrast(p=0.3),
    A.GaussNoise(p=0.2),
    A.RandomCrop(width=512, height=512, p=0.5),
], bbox_params=A.BboxParams(
    format='coco',              # [x, y, w, h]
    label_fields=['category_ids'],
    min_area=100,               # 小さすぎるBOXを除外
    min_visibility=0.5          # 見切れが半分以上のBOXを除外
))

PyTorchデータローダーの実装

import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os
import json

class CustomImageDataset(Dataset):
    """カスタムデータセットクラス"""
    
    def __init__(self, root_dir, split='train', transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.samples = []
        self.class_to_idx = {}
        
        # クラスディレクトリからサンプルを収集
        classes = sorted(os.listdir(
            os.path.join(root_dir, split)
        ))
        self.class_to_idx = {cls: i for i, cls in enumerate(classes)}
        
        for cls in classes:
            cls_dir = os.path.join(root_dir, split, cls)
            for fname in os.listdir(cls_dir):
                if fname.endswith(('.jpg', '.png', '.jpeg')):
                    self.samples.append({
                        'path': os.path.join(cls_dir, fname),
                        'label': self.class_to_idx[cls]
                    })
        
        print(f"{split}セット: {len(self.samples)}枚, "
              f"{len(classes)}クラス")
    
    def __len__(self):
        return len(self.samples)
    
    def __getitem__(self, idx):
        sample = self.samples[idx]
        image = Image.open(sample['path']).convert('RGB')
        
        if self.transform:
            image = self.transform(image)
        
        return image, sample['label']

# データセット作成
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ColorJitter(0.2, 0.2, 0.2, 0.1),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225])
])

train_dataset = CustomImageDataset('./data', 'train', train_transform)
train_loader = DataLoader(
    train_dataset,
    batch_size=32,
    shuffle=True,
    num_workers=4,
    pin_memory=True  # GPU転送を高速化
)

エッジAI向けデータ収集

エッジデバイスでのデータ収集は、Edge Impulseなどのプラットフォームを使って行えます。

# Edge Impulse Python SDKでのデータアップロード
import requests
import json

def upload_sample_to_edge_impulse(
    api_key, label, data, sampling_freq=100
):
    """センサーデータをEdge Impulseにアップロード"""
    
    payload = {
        "protected": {
            "ver": "v1",
            "alg": "none",
            "iat": int(time.time())
        },
        "signature": "0000",
        "payload": {
            "device_name": "my_sensor",
            "device_type": "IMU",
            "interval_ms": 1000 / sampling_freq,
            "sensors": [
                {"name": "accX", "units": "m/s2"},
                {"name": "accY", "units": "m/s2"},
                {"name": "accZ", "units": "m/s2"}
            ],
            "values": data.tolist()
        }
    }
    
    response = requests.post(
        'https://ingestion.edgeimpulse.com/api/training/data',
        headers={
            'x-api-key': api_key,
            'x-label': label,
            'Content-Type': 'application/json'
        },
        data=json.dumps(payload)
    )
    return response.status_code == 200

用途・ユースケース

エッジAI向け主要データセット一覧

画像・ビジョン:

データセットクラス数サイズ用途
CIFAR-101060K枚軽量分類ベンチマーク
Google Speech Commands35105Kウェイクワード検出
COCO(検出)80330K枚物体検出標準
MVTec AD155354枚工業異常検知
UC Merced(航空写真)212100枚ドローン・農業

センサー・時系列:

データセットセンサーサンプル数用途
UCI HAR加速度・ジャイロ10K+活動認識
CWRU Bearing振動センサー多数軸受け故障診断
MIT-BIHECG47名分不整脈検知

組み込み向けデータセット構築のポイント

工業向け組み込みAIでは、標準データセットではなく自前でデータ収集することがほとんどです。

データ収集のベストプラクティス:
1. 実運用環境と同じ条件でデータ収集
   (照明・角度・距離・季節変動を考慮)
2. 多様なバリエーションを含める
   (複数の作業者・複数ロット・複数時間帯)
3. エッジケース(境界事例)を意識的に収集
4. 検収条件を事前に設定(精度・再現率の目標)
5. アノテーションガイドラインを文書化

実装・開発のポイント

クラス不均衡への対処

import torch
from torch.utils.data import WeightedRandomSampler

def create_balanced_sampler(dataset):
    """クラス不均衡をサンプリングで解消"""
    class_counts = Counter([sample[1] for sample in dataset])
    weights = {cls: 1.0 / count 
               for cls, count in class_counts.items()}
    sample_weights = [weights[sample[1]] for sample in dataset]
    
    sampler = WeightedRandomSampler(
        weights=sample_weights,
        num_samples=len(sample_weights),
        replacement=True
    )
    return sampler

# 損失関数の重み付けでも対処可能
class_weights = torch.FloatTensor(
    [1.0 / class_counts[i] for i in range(num_classes)]
)
criterion = nn.CrossEntropyLoss(weight=class_weights)

データセットの検証

def validate_dataset(dataset_dir):
    """データセットの整合性チェック"""
    issues = []
    
    for split in ['train', 'val', 'test']:
        split_dir = os.path.join(dataset_dir, split)
        if not os.path.exists(split_dir):
            issues.append(f"分割ディレクトリ不存在: {split}")
            continue
        
        # 各クラスのサンプル数確認
        class_counts = {}
        for cls in os.listdir(split_dir):
            cls_dir = os.path.join(split_dir, cls)
            if os.path.isdir(cls_dir):
                count = len([f for f in os.listdir(cls_dir)
                             if f.endswith(('.jpg', '.png'))])
                class_counts[cls] = count
                if count < 10:
                    issues.append(
                        f"{split}/{cls}: サンプル数が少ない ({count}枚)"
                    )
        
        print(f"{split}: {sum(class_counts.values())}枚, "
              f"{len(class_counts)}クラス")
    
    if issues:
        print("\n問題点:")
        for issue in issues:
            print(f"  - {issue}")
    else:
        print("\nデータセット検証OK")
    
    return len(issues) == 0

他技術との比較

データ源コスト品質ドメイン適合性
公開データセット無料多い高い汎用(要ドメイン適応)
合成データ制限なし中(リアリズムの問題)高い
実機収集高い限られる最高最高
データ購入非常に高い中〜多い高い中〜高い

データセットは学習済みモデルと並んでエッジAI開発の根幹をなす資産です。適切なデータセット設計と収集なしには、物体検出異常検知のいずれも実用水準の精度に達しません。TinyML向けには特に収集コストを意識した効率的なデータ戦略が求められます。

関連用語

参考リンク