Z-Score正規化:数学原理と実装ガイド

Z-Score正規化:詳細な数学的背景と実装

一、数学的基礎と導出

1.1 基本式

Z-Score正規化(標準偏差正規化)は、データを平均0、標準偏差1の標準正規分布に変換する手法です:

[z = \frac{x - \mu}{\sigma} ]ここで、

  • \(\mu\) はデータの平均値:\(\mu = \frac{1}{n}\sum_{i=1}^{n} x_i\)
  • \(\sigma\) はデータの標準偏差:\(\sigma = \sqrt{\frac{1}{n}\sum_{i=1}^{n} (x_i - \mu)^2}\)

1.2 標本標準偏差と母集団標準偏差

実際の応用では、以下の区別に注意が必要です:

  • 母集団標準偏差:\(\sigma = \sqrt{\frac{1}{n}\sum_{i=1}^{n} (x_i - \mu)^2}\)
  • 標本標準偏差:\(s = \sqrt{\frac{1}{n-1}\sum_{i=1}^{n} (x_i - \bar{x})^2}\)

Z-Score正規化では通常、母集団標準偏差を使用します。

1.3 行列形式での表現

データセット \(X \in \mathbb{R}^{m \times n}\)(m個のサンプル、n個の特徴量)に対して:

[Z = \frac{X - \mu}{\sigma} ]ここで、

  • \(\mu\):各列の平均値ベクトル \(\in \mathbb{R}^{1 \times n}\)
  • \(\sigma\):各列の標準偏差ベクトル \(\in \mathbb{R}^{1 \times n}\)
  • 演算はブロードキャスト操作です

二、数学的性質の分析

2.1 変換の性質

\(T(x) = \frac{x - \mu}{\sigma}\) とすると、

  1. 線形変換:\(T(ax + b) = a'T(x) + b'\)、ここで \(a' = \frac{a}{\sigma}\)、\(b' = \frac{b}{\sigma}\)
  2. 分布形状の保持:もし \(X \sim N(\mu, \sigma^2)\) ならば、\(Z \sim N(0, 1)\)
  3. 無次元化:異なる特徴量間の単位の影響を排除

2.2 統計的性質の証明

定理:任意のデータセット \(X = \{x_1, x_2, ..., x_n\}\) に対して、Z-Score正規化後のデータを \(Z = \{z_1, z_2, ..., z_n\}\) とすると、

  1. \(\bar{z} = 0\)
  2. \(s_z = 1\)

証明

  1. 平均値:

[\bar{z} = \frac{1}{n}\sum_{i=1}^{n} z_i = \frac{1}{n}\sum_{i=1}^{n} \frac{x_i - \mu}{\sigma} = \frac{1}{\sigma}\left(\frac{1}{n}\sum_{i=1}^{n} x_i - \mu\right) = 0 ] 2. 標準偏差:

[s_z^2 = \frac{1}{n}\sum_{i=1}^{n} (z_i - \bar{z})^2 = \frac{1}{n}\sum_{i=1}^{n} z_i^2 = \frac{1}{n}\sum_{i=1}^{n} \left(\frac{x_i - \mu}{\sigma}\right)^2 = \frac{1}{\sigma^2} \cdot \frac{1}{n}\sum_{i=1}^{n} (x_i - \mu)^2 = 1 ]

三、実装と数学的検証

3.1 Z-Score正規化の実装

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.preprocessing import StandardScaler

class DataNormalizer:
    """Z-Score正規化の実装クラス"""
    
    def __init__(self, freedom_degree=0):
        """
        パラメータ:
        freedom_degree: 自由度の調整、0は母集団標準偏差、1は標本標準偏差
        """
        self.freedom_degree = freedom_degree
        self.average = None
        self.deviation = None
        
    def fit(self, data):
        """
        データの平均値と標準偏差を計算
        
        パラメータ:
        data: 元のデータ、形状は(n_samples, n_features)または(n_samples,)
        """
        data = np.array(data)
        if data.ndim == 1:
            data = data.reshape(-1, 1)
            
        self.average = np.mean(data, axis=0)
        self.deviation = np.std(data, axis=0, ddof=self.freedom_degree)
        
        # ゼロ除算を防ぐ
        self.deviation[self.deviation == 0] = 1
        
        return self
    
    def transform(self, data):
        """
        Z-Score正規化を適用
        
        式: z = (x - average) / deviation
        """
        if self.average is None or self.deviation is None:
            raise ValueError("まずfitメソッドを呼び出す必要があります")
            
        data = np.array(data)
        if data.ndim == 1:
            data = data.reshape(-1, 1)
            
        # 式を適用
        normalized_data = (data - self.average) / self.deviation
            
        return normalized_data.squeeze()
    
    def fit_transform(self, data):
        """fitとtransformの組み合わせ"""
        return self.fit(data).transform(data)
    
    def reverse_transform(self, normalized_data):
        """
        逆正規化
        
        式: x = z * deviation + average
        """
        if self.average is None or self.deviation is None:
            raise ValueError("まずfitメソッドを呼び出す必要があります")
            
        normalized_data = np.array(normalized_data)
        if normalized_data.ndim == 1:
            normalized_data = normalized_data.reshape(-1, 1)
            
        # 逆変換式を適用
        original_data = normalized_data * self.deviation + self.average
        
        return original_data.squeeze()
    
    def get_parameters(self):
        """正規化パラメータを取得"""
        return {
            'average': self.average,
            'deviation': self.deviation,
            'freedom_degree': self.freedom_degree
        }

3.2 数学的検証とテスト

def validate_zscore_properties():
    """Z-Score正規化の数学的性質を検証"""
    
    # 1. テストデータの生成
    np.random.seed(42)
    data = np.random.normal(loc=50, scale=10, size=1000)  # 正規分布
    
    # 2. 正規化器の初期化
    normalizer = DataNormalizer()
    normalized = normalizer.fit_transform(data)
    
    # 3. 性質1の検証: 平均値が0、標準偏差が1
    print("性質1の検証 - 統計的性質:")
    print(f"  元のデータ: μ={data.mean():.6f}, σ={data.std(ddof=0):.6f}")
    print(f"  正規化データ: μ={normalized.mean():.6f}, σ={normalized.std(ddof=0):.6f}")
    print(f"  μ=0を満足: {abs(normalized.mean()) < 1e-10}")
    print(f"  σ=1を満足: {abs(normalized.std(ddof=0) - 1) < 1e-10}")
    
    # 4. 性質2の検証: 線形変換の性質
    print("\n性質2の検証 - 線形変換:")
    a, b = 2, 3
    linear_data = a * data + b
    
    # 方法1: linear_dataを直接正規化
    normalizer2 = DataNormalizer()
    linear_normalized = normalizer2.fit_transform(linear_data)
    
    # 方法2: 正規化normalizedから計算
    # 理論的導出: もし Y = aX + b ならば、Z_Y = (Y - μ_Y)/σ_Y = (aX + b - aμ_X - b)/(aσ_X) = (X - μ_X)/σ_X = Z_X
    theoretical_normalized = normalized  # 理論的には同じ
    
    # 誤差を計算
    error = np.max(np.abs(linear_normalized - theoretical_normalized))
    print(f"  線形変換正規化誤差: {error:.10f}")
    print(f"  線形関係を保持: {error < 1e-10}")
    
    # 5. 性質3の検証: 可逆性
    print("\n性質3の検証 - 可逆性:")
    reconstructed_data = normalizer.reverse_transform(normalized)
    reconstruction_error = np.max(np.abs(data - reconstructed_data))
    print(f"  最大再構成誤差: {reconstruction_error:.10f}")
    print(f"  可逆性: {reconstruction_error < 1e-10}")
    
    # 6. 性質4の検証: 分布形状の保持
    print("\n性質4の検証 - 分布形状の保持:")
    
    # Kolmogorov-Smirnov検定
    from scipy.stats import kstest
    
    # 元のデータが正規分布か検定
    ks_stat_original, p_original = kstest(data, 'norm', args=(data.mean(), data.std()))
    print(f"  元のデータKS検定: 統計量={ks_stat_original:.6f}, p値={p_original:.6f}")
    
    # 正規化データが標準正規分布か検定
    ks_stat_z, p_z = kstest(normalized, 'norm', args=(0, 1))
    print(f"  正規化データKS検定: 統計量={ks_stat_z:.6f}, p値={p_z:.6f}")
    
    # p値が0.05より大きい場合、正規分布の仮説を棄却できない
    print(f"  元のデータが正規分布: {p_original > 0.05}")
    print(f"  正規化データが標準正規分布: {p_z > 0.05}")
    
    # 7. 標本標準偏差と母集団標準偏差の違いを検証
    print("\n標本vs母集団標準偏差検証:")
    
    # 母集団標準偏差
    normalizer_pop = DataNormalizer(freedom_degree=0)
    normalized_pop = normalizer_pop.fit_transform(data)
    
    # 標本標準偏差
    normalizer_sample = DataNormalizer(freedom_degree=1)
    normalized_sample = normalizer_sample.fit_transform(data)
    
    print(f"  母集団標準偏差: {normalizer_pop.deviation}")
    print(f"  標本標準偏差: {normalizer_sample.deviation}")
    print(f"  母集団標準偏差正規化後σ: {normalized_pop.std(ddof=0):.6f}")
    print(f"  標本標準偏差正規化後σ: {normalized_sample.std(ddof=0):.6f}")
    
    # 標本標準偏差で正規化後、母集団公式で計算したσ
    sigma_sample_std = normalized_sample.std(ddof=0)
    print(f"  標本標準偏差で正規化、母集団公式で計算したσ: {sigma_sample_std:.6f}")

validate_zscore_properties()

四、幾何学的解釈と可視化

4.1 1次元データの可視化

def visualize_zscore_1d():
    """1次元データのZ-Score正規化を可視化"""
    
    # 異なる分布のデータを生成
    np.random.seed(42)
    
    # 正規分布
    normal_data = np.random.normal(loc=50, scale=10, size=1000)
    
    # 一様分布
    uniform_data = np.random.uniform(20, 80, size=1000)
    
    # 指数分布
    exponential_data = np.random.exponential(scale=20, size=1000) + 30
    
    datasets = [
        ("正規分布", normal_data),
        ("一様分布", uniform_data),
        ("指数分布", exponential_data)
    ]
    
    # グラフを作成
    fig, axes = plt.subplots(3, 4, figsize=(16, 12))
    
    for idx, (name, data) in enumerate(datasets):
        # 正規化
        normalizer = DataNormalizer()
        normalized = normalizer.fit_transform(data)
        
        # 1. 元のデータのヒストグラム
        axes[idx, 0].hist(data, bins=50, density=True, alpha=0.7, color='blue')
        axes[idx, 0].axvline(x=data.mean(), color='red', linestyle='-', 
                            linewidth=2, label=f'μ={data.mean():.2f}')
        axes[idx, 0].axvline(x=data.mean() + data.std(), color='green', 
                            linestyle='--', linewidth=1.5, 
                            label=f'μ±σ=[{data.mean()-data.std():.2f}, {data.mean()+data.std():.2f}]')
        axes[idx, 0].axvline(x=data.mean() - data.std(), color='green', 
                            linestyle='--', linewidth=1.5)
        axes[idx, 0].set_xlabel('データ値')
        axes[idx, 0].set_ylabel('密度')
        axes[idx, 0].set_title(f'{name} - 元のデータ')
        axes[idx, 0].legend()
        axes[idx, 0].grid(True, alpha=0.3)
        
        # 2. 正規化過程の図示
        data_range = np.linspace(data.min(), data.max(), 1000)
        normalized_range = (data_range - normalizer.average) / normalizer.deviation
        
        axes[idx, 1].plot(data_range, normalized_range, 'b-', linewidth=2)
        
        # 重要な点をマーク
        key_points = [data.mean(), data.mean() + data.std(), data.mean() - data.std()]
        for point in key_points:
            normalized_point = (point - normalizer.average) / normalizer.deviation
            axes[idx, 1].plot([point, point], [0, normalized_point], 'r--', alpha=0.5)
            axes[idx, 1].plot(point, normalized_point, 'ro', markersize=8)
            axes[idx, 1].text(point, normalized_point+0.5, f'({point:.1f}, {normalized_point:.1f})', 
                             fontsize=9, ha='center')
        
        axes[idx, 1].set_xlabel('元のデータ値')
        axes[idx, 1].set_ylabel('正規化Z値')
        axes[idx, 1].set_title(f'{name} - 正規化マッピング関数')
        axes[idx, 1].grid(True, alpha=0.3)
        
        # 3. 正規化後データのヒストグラム
        axes[idx, 2].hist(normalized, bins=50, density=True, alpha=0.7, color='green')
        
        # 標準正規分布曲線を描画
        x_norm = np.linspace(-4, 4, 1000)
        y_norm = stats.norm.pdf(x_norm, 0, 1)
        axes[idx, 2].plot(x_norm, y_norm, 'r-', linewidth=2, label='標準正規分布')
        
        axes[idx, 2].axvline(x=0, color='red', linestyle='-', 
                           linewidth=2, label='μ=0')
        axes[idx, 2].axvline(x=1, color='green', linestyle='--', 
                           linewidth=1.5, label='σ=1')
        axes[idx, 2].axvline(x=-1, color='green', linestyle='--', linewidth=1.5)
        
        axes[idx, 2].set_xlabel('正規化Z値')
        axes[idx, 2].set_ylabel('密度')
        axes[idx, 2].set_title(f'{name} - 正規化後データ')
        axes[idx, 2].set_xlim(-4, 4)
        axes[idx, 2].legend()
        axes[idx, 2].grid(True, alpha=0.3)
        
        # 4. QQプロット(分位数-分位数プロット)
        stats.probplot(normalized, dist="norm", plot=axes[idx, 3])
        axes[idx, 3].set_title(f'{name} - QQプロット')
        axes[idx, 3].grid(True, alpha=0.3)
        
        # R²値を計算
        from scipy.stats import pearsonr
        osr = stats.zscore(normalized)
        r, _ = pearsonr(osr, normalized)
        axes[idx, 3].text(0.05, 0.95, f'R² = {r**2:.4f}', 
                         transform=axes[idx, 3].transAxes,
                         bbox=dict(boxstyle="round,pad=0.3", 
                                 facecolor="yellow", alpha=0.5))
    
    plt.tight_layout()
    plt.show()
    
    # 数学的分析
    print("Z-Score正規化の数学的分析:")
    print("式: z = (x - μ) / σ")
    print()
    
    for name, data in datasets:
        μ, σ = data.mean(), data.std(ddof=0)
        print(f"{name}:")
        print(f"  元の統計: μ={μ:.4f}, σ={σ:.4f}")
        print(f"  正規化後: 理論μ=0, σ=1")
        print(f"  実際検証: μ={(data-μ)/σ}.mean():.6f}, σ={((data-μ)/σ).std():.6f}")
        print()

visualize_zscore_1d()

4.2 2次元データの可視化

def visualize_zscore_2d():
    """2次元データのZ-Score正規化を可視化"""
    
    # 2次元データを生成
    np.random.seed(42)
    n_samples = 500
    
    # 2つの相関のある特徴量を作成
    feature1 = np.random.normal(loc=50, scale=10, size=n_samples)
    feature2 = 0.7 * feature1 + np.random.normal(loc=0, scale=5, size=n_samples)
    data = np.column_stack([feature1, feature2])
    
    # 外れ値をいくつ追加
    data[0, 0] = 150  # 外れ値
    data[1, 1] = -20  # 外れ値
    
    # 正規化
    normalizer = DataNormalizer()
    normalized = normalizer.fit_transform(data)
    
    # グラフを作成
    fig, axes = plt.subplots(2, 3, figsize=(15, 10))
    
    # 1. 元のデータの散布図
    scatter1 = axes[0, 0].scatter(data[:, 0], data[:, 1], alpha=0.6, 
                                  c=np.arange(n_samples), cmap='viridis')
    axes[0, 0].axhline(y=data[:, 1].mean(), color='red', linestyle='-', 
                      linewidth=2, label=f'μ2={data[:, 1].mean():.2f}')
    axes[0, 0].axvline(x=data[:, 0].mean(), color='blue', linestyle='-', 
                      linewidth=2, label=f'μ1={data[:, 0].mean():.2f}')
    
    # 1σ楕円を追加
    from matplotlib.patches import Ellipse
    cov_matrix = np.cov(data.T)
    eigvals, eigvecs = np.linalg.eig(cov_matrix)
    angle = np.degrees(np.arctan2(eigvecs[1, 0], eigvecs[0, 0]))
    width = 2 * np.sqrt(eigvals[0])
    height = 2 * np.sqrt(eigvals[1])
    
    ellipse = Ellipse(xy=(data[:, 0].mean(), data[:, 1].mean()), 
                     width=width, height=height, angle=angle,
                     edgecolor='red', facecolor='none', linewidth=2, 
                     linestyle='--', label='1σ楕円')
    axes[0, 0].add_patch(ellipse)
    
    axes[0, 0].set_xlabel('特徴量1')
    axes[0, 0].set_ylabel('特徴量2')
    axes[0, 0].set_title('元のデータの散布図(外れ値含む)')
    axes[0, 0].legend(loc='upper right')
    axes[0, 0].grid(True, alpha=0.3)
    
    # 2. 特徴量分布の比較
    axes[0, 1].hist(data[:, 0], bins=30, alpha=0.5, label='特徴量1', density=True)
    axes[0, 1].hist(data[:, 1], bins=30, alpha=0.5, label='特徴量2', density=True)
    axes[0, 1].set_xlabel('特徴量値')
    axes[0, 1].set_ylabel('密度')
    axes[0, 1].set_title('元の特徴量分布の比較')
    axes[0, 1].legend()
    axes[0, 1].grid(True, alpha=0.3)
    
    # 3. 正規化過程の図示
    # 元の座標系と正規化後座標系を描画
    axes[0, 2].scatter(data[:, 0], data[:, 1], alpha=0.3, label='元のデータ')
    
    # 正規化後の位置を描画
    axes[0, 2].scatter(normalized[:, 0] * 20 + data[:, 0].mean(),  # 可視化のためにスケール
                      normalized[:, 1] * 20 + data[:, 1].mean(),
                      alpha=0.3, color='red', label='正規化後位置')
    
    # マッピング線を描画
    for i in range(min(10, n_samples)):  # 最初の10個のみ表示
        axes[0, 2].plot([data[i, 0], normalized[i, 0] * 20 + data[:, 0].mean()],
                       [data[i, 1], normalized[i, 1] * 20 + data[:, 1].mean()],
                       'g--', alpha=0.3)
    
    axes[0, 2].set_xlabel('特徴量1')
    axes[0, 2].set_ylabel('特徴量2')
    axes[0, 2].set_title('Z-Scoreマッピング過程図示')
    axes[0, 2].legend()
    axes[0, 2].grid(True, alpha=0.3)
    
    # 4. 正規化後データの散布図
    scatter4 = axes[1, 0].scatter(normalized[:, 0], normalized[:, 1], alpha=0.6, 
                                  c=np.arange(n_samples), cmap='viridis')
    axes[1, 0].axhline(y=0, color='red', linestyle='-', linewidth=2, 
                      label='μ2=0')
    axes[1, 0].axvline(x=0, color='blue', linestyle='-', linewidth=2, 
                      label='μ1=0')
    
    # 1σ円を追加
    circle = plt.Circle((0, 0), 1, edgecolor='red', facecolor='none', 
                       linewidth=2, linestyle='--', label='1σ円')
    axes[1, 0].add_patch(circle)
    
    axes[1, 0].set_xlabel('正規化特徴量1')
    axes[1, 0].set_ylabel('正規化特徴量2')
    axes[1, 0].set_title('正規化後データの散布図')
    axes[1, 0].set_xlim(-4, 4)
    axes[1, 0].set_ylim(-4, 4)
    axes[1, 0].legend(loc='upper right')
    axes[1, 0].grid(True, alpha=0.3)
    
    # 5. 正規化後特徴量分布の比較
    axes[1, 1].hist(normalized[:, 0], bins=30, alpha=0.5, label='正規化特徴量1', 
                   density=True)
    axes[1, 1].hist(normalized[:, 1], bins=30, alpha=0.5, label='正規化特徴量2', 
                   density=True)
    
    # 標準正規分布曲線を描画
    x_norm = np.linspace(-4, 4, 1000)
    y_norm = stats.norm.pdf(x_norm, 0, 1)
    axes[1, 1].plot(x_norm, y_norm, 'k-', linewidth=2, 
                   label='標準正規分布')
    
    axes[1, 1].set_xlabel('正規化特徴量値')
    axes[1, 1].set_ylabel('密度')
    axes[1, 1].set_title('正規化後特徴量分布の比較')
    axes[1, 1].set_xlim(-4, 4)
    axes[1, 1].legend()
    axes[1, 1].grid(True, alpha=0.3)
    
    # 6. 相関性の保持検証
    corr_original = np.corrcoef(data.T)[0, 1]
    corr_normalized = np.corrcoef(normalized.T)[0, 1]
    
    axes[1, 2].scatter(data[:, 0], data[:, 1], alpha=0.5, label=f'元のデータ(r={corr_original:.4f})')
    axes[1, 2].scatter(normalized[:, 0], normalized[:, 1], alpha=0.5, label=f'正規化データ(r={corr_normalized:.4f})')
    axes[1, 2].set_xlabel('特徴量1 / 正規化特徴量1')
    axes[1, 2].set_ylabel('特徴量2 / 正規化特徴量2')
    axes[1, 2].set_title('相関性保持検証')
    axes[1, 2].legend()
    axes[1, 2].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    # 統計情報
    print("元のデータの統計:")
    print(f"  特徴量1: μ={data[:, 0].mean():.4f}, σ={data[:, 0].std():.4f}")
    print(f"  特徴量2: μ={data[:, 1].mean():.4f}, σ={data[:, 1].std():.4f}")
    print(f"  共分散行列:\n{np.cov(data.T)}")
    print(f"  相関係数: {corr_original:.6f}")
    
    print("\n正規化後データの統計:")
    print(f"  特徴量1: μ={normalized[:, 0].mean():.6f}, σ={normalized[:, 0].std():.6f}")
    print(f"  特徴量2: μ={normalized[:, 1].mean():.6f}, σ={normalized[:, 1].std():.6f}")
    print(f"  共分散行列:\n{np.cov(normalized.T)}")
    print(f"  相関係数: {corr_normalized:.6f}")
    
    print(f"\n相関係数の変化: {abs(corr_original - corr_normalized):.10f}")
    print("Z-Score正規化はデータの相関性を保持します!")

visualize_zscore_2d()

五、数学的定理と証明

5.1 Z-Score正規化は相関性を保持する

定理:任意の2変数 \(X\) と \(Y\) に対して、Z-Score正規化後のデータを \(Z_X\) と \(Z_Y\) とすると、相関係数は不变です:

[\rho_{X,Y} = \rho_{Z_X, Z_Y} ]証明: 相関係数は次のように定義されます:

[\rho_{X,Y} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} ]正規化後:

[Z_X = \frac{X - \mu_X}{\sigma_X}, \quad Z_Y = \frac{Y - \mu_Y}{\sigma_Y} ]共分散を計算:

[\begin{aligned} \text{Cov}(Z_X, Z_Y) &= \mathbb{E}[(Z_X - \mathbb{E}[Z_X])(Z_Y - \mathbb{E}[Z_Y])] \ &= \mathbb{E}[Z_X Z_Y] \quad (\text{なぜなら } \mathbb{E}[Z_X] = \mathbb{E}[Z_Y] = 0) \ &= \mathbb{E}\left[\frac{X - \mu_X}{\sigma_X} \cdot \frac{Y - \mu_Y}{\sigma_Y}\right] \ &= \frac{1}{\sigma_X \sigma_Y} \mathbb{E}[(X - \mu_X)(Y - \mu_Y)] \ &= \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} \end{aligned} ]そして \(\sigma_{Z_X} = \sigma_{Z_Y} = 1\) なので、

[\rho_{Z_X, Z_Y} = \frac{\text{Cov}(Z_X, Z_Y)}{\sigma_{Z_X} \sigma_{Z_Y}} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} = \rho_{X,Y} ]### 5.2 Z-Score正規化と線形変換

定理:Z-Score正規化は線形変換です。

証明: \(T(X) = \frac{X - \mu_X}{\sigma_X}\) とすると、任意の \(a, b \in \mathbb{R}\) に対して、

[T(aX + b) = \frac{aX + b - \mu_{aX+b}}{\sigma_{aX+b}} ]既知:

  • \(\mu_{aX+b} = a\mu_X + b\)
  • \(\sigma_{aX+b} = |a|\sigma_X\)

したがって、

[T(aX + b) = \frac{aX + b - (a\mu_X + b)}{|a|\sigma_X} = \frac{a(X - \mu_X)}{|a|\sigma_X} = \text{sign}(a) \cdot \frac{X - \mu_X}{\sigma_X} = \text{sign}(a) \cdot T(X) ]よって、Z-Score正規化は方向を保持する線形変換です。

5.3 Z-Scoreとマハラノビス距離

多次元データ \(X \in \mathbb{R}^{m \times n}\) に対して、マハラノビス距離は次のように定義されます:

[D_M(x) = \sqrt{(x - \mu)^T \Sigma^{-1} (x - \mu)} ]ここで \(\Sigma\) は共分散行列です。

特徴量が相関しない場合、\(\Sigma\) は対角行列で、対角要素は \(\sigma_i^2\) です。すると、

[D_M(x) = \sqrt{\sum_{i=1}^n \frac{(x_i - \mu_i)^2}{\sigma_i^2}} = \sqrt{\sum_{i=1}^n z_i^2} = |z| ]したがって、Z-Score正規化後のユークリッド距離は、元データのマハラノビス距離に等しくなります。

六、機械学習におけるZ-Scoreの応用

6.1 ニューラルネットワークへの応用

def neural_network_example():
    """Z-Score正規化を使用したニューラルネットワークの例"""
    
    # 1. 非線形データの生成
    np.random.seed(42)
    n_samples = 500
    
    # データ生成: y = x² + ノイズ
    input_data = np.random.uniform(-3, 3, n_samples)
    output_data = input_data**2 + np.random.normal(0, 1, n_samples)
    
    # 2. 訓練セットとテストセットに分割
    from sklearn.model_selection import train_test_split
    train_input, test_input, train_output, test_output = train_test_split(
        input_data, output_data, test_size=0.2, random_state=42
    )
    
    # 3. input_dataとoutput_dataをそれぞれZ-Score正規化
    input_normalizer = DataNormalizer()
    output_normalizer = DataNormalizer()
    
    train_input_norm = input_normalizer.fit_transform(train_input.reshape(-1, 1))
    train_output_norm = output_normalizer.fit_transform(train_output.reshape(-1, 1))
    
    test_input_norm = input_normalizer.transform(test_input.reshape(-1, 1))
    
    # 4. より複雑なニューラルネットワークを構築
    class NeuralNetwork:
        def __init__(self, input_dim=1, hidden_dims=[10, 10], output_dim=1):
            # 重みの初期化
            np.random.seed(42)
            self.weights = []
            self.biases = []
            
            # 入力層から最初の隠れ層へ
            prev_dim = input_dim
            for hidden_dim in hidden_dims:
                self.weights.append(np.random.randn(prev_dim, hidden_dim) * 0.1)
                self.biases.append(np.zeros((1, hidden_dim)))
                prev_dim = hidden_dim
            
            # 最後の隠れ層から出力層へ
            self.weights.append(np.random.randn(prev_dim, output_dim) * 0.1)
            self.biases.append(np.zeros((1, output_dim)))
            
            self.activations = []
            
        def relu(self, x):
            return np.maximum(0, x)
        
        def relu_derivative(self, x):
            return (x > 0).astype(float)
        
        def forward(self, X):
            """順伝播"""
            self.activations = [X]
            a = X
            
            for i in range(len(self.weights) - 1):
                z = np.dot(a, self.weights[i]) + self.biases[i]
                a = self.relu(z)
                self.activations.append(a)
            
            # 出力層(線形活性化)
            z = np.dot(a, self.weights[-1]) + self.biases[-1]
            self.activations.append(z)
            
            return z
        
        def backward(self, X, y, y_pred, lr=0.01):
            """逆伝播"""
            m = X.shape[0]
            
            # 出力層の誤差
            d_loss = 2 * (y_pred - y) / m
            
            # 逆伝播
            d_weights = []
            d_biases = []
            
            # 出力層の勾配
            dZ = d_loss
            dW = np.dot(self.activations[-2].T, dZ)
            db = np.sum(dZ, axis=0, keepdims=True)
            d_weights.insert(0, dW)
            d_biases.insert(0, db)
            
            # 隠れ層の勾配
            for i in range(len(self.weights) - 2, -1, -1):
                dA = np.dot(dZ, self.weights[i + 1].T)
                dZ = dA * self.relu_derivative(self.activations[i + 1])
                dW = np.dot(self.activations[i].T, dZ)
                db = np.sum(dZ, axis=0, keepdims=True)
                d_weights.insert(0, dW)
                d_biases.insert(0, db)
            
            # 重みの更新
            for i in range(len(self.weights)):
                self.weights[i] -= lr * d_weights[i]
                self.biases[i] -= lr * d_biases[i]
        
        def predict(self, X):
            """予測"""
            return self.forward(X)
    
    # 5. ニューラルネットワークの訓練
    nn = NeuralNetwork(input_dim=1, hidden_dims=[20, 20], output_dim=1)
    epochs = 5000
    losses = []
    
    for epoch in range(epochs):
        # 順伝播
        predicted_norm = nn.forward(train_input_norm.reshape(-1, 1))
        
        # 損失の計算
        loss = np.mean((predicted_norm - train_output_norm.reshape(-1, 1)) ** 2)
        losses.append(loss)
        
        # 逆伝播
        nn.backward(train_input_norm.reshape(-1, 1), 
                   train_output_norm.reshape(-1, 1), 
                   predicted_norm, 
                   lr=0.01)
        
        if epoch % 1000 == 0:
            print(f"Epoch {epoch}: Loss = {loss:.6f}")
    
    # 6. 予測と逆正規化
    predicted_test_norm = nn.predict(test_input_norm.reshape(-1, 1))
    predicted_test = output_normalizer.reverse_transform(predicted_test_norm)
    
    # 7. 結果の可視化
    fig, axes = plt.subplots(2, 3, figsize=(15, 10))
    
    # 元のデータ
    axes[0, 0].scatter(train_input, train_output, alpha=0.5, label='訓練データ')
    axes[0, 0].scatter(test_input, test_output, alpha=0.5, color='red', label='テストデータ')
    plot_input = np.linspace(-3, 3, 1000)
    true_output = plot_input**2
    axes[0, 0].plot(plot_input, true_output, 'g-', linewidth=2, label='真の関数: y=x²')
    axes[0, 0].set_xlabel('入力X')
    axes[0, 0].set_ylabel('出力y')
    axes[0, 0].set_title('元のデータ分布')
    axes[0, 0].legend()
    axes[0, 0].grid(True, alpha=0.3)
    
    # 正規化後データ
    plot_input_norm = input_normalizer.transform(plot_input.reshape(-1, 1))
    true_output_norm = output_normalizer.transform(true_output.reshape(-1, 1))
    
    axes[0, 1].scatter(train_input_norm, train_output_norm, alpha=0.5, label='正規化訓練データ')
    axes[0, 1].scatter(test_input_norm, output_normalizer.transform(test_output.reshape(-1, 1)), 
                      alpha=0.5, color='red', label='正規化テストデータ')
    axes[0, 1].plot(plot_input_norm, true_output_norm, 'g-', linewidth=2, 
                   label='正規化真の関数')
    axes[0, 1].set_xlabel('正規化入力X')
    axes[0, 1].set_ylabel('正規化出力y')
    axes[0, 1].set_title('正規化後データ分布')
    axes[0, 1].legend()
    axes[0, 1].grid(True, alpha=0.3)
    
    # 損失曲線
    axes[0, 2].plot(losses)
    axes[0, 2].set_xlabel('Epoch')
    axes[0, 2].set_ylabel('Loss (MSE)')
    axes[0, 2].set_title('訓練損失曲線')
    axes[0, 2].set_yscale('log')
    axes[0, 2].grid(True, alpha=0.3)
    
    # 予測結果の比較
    axes[1, 0].scatter(test_input, test_output, alpha=0.5, label='テストデータ')
    axes[1, 0].scatter(test_input, predicted_test, alpha=0.8, color='red', 
                      s=20, label='ニューラルネットワーク予測')
    axes[1, 0].plot(plot_input, true_output, 'g-', linewidth=2, label='真の関数')
    axes[1, 0].set_xlabel('入力X')
    axes[1, 0].set_ylabel('出力y')
    axes[1, 0].set_title('ニューラルネットワーク予測結果')
    axes[1, 0].legend()
    axes[1, 0].grid(True, alpha=0.3)
    
    # 残差分析
    residuals = test_output - predicted_test
    axes[1, 1].scatter(predicted_test, residuals, alpha=0.5)
    axes[1, 1].axhline(y=0, color='red', linestyle='-', linewidth=2)
    axes[1, 1].set_xlabel('予測値')
    axes[1, 1].set_ylabel('残差')
    axes[1, 1].set_title('残差分析図')
    axes[1, 1].grid(True, alpha=0.3)
    
    # 予測誤差の分布
    axes[1, 2].hist(residuals, bins=30, density=True, alpha=0.7)
    axes[1, 2].set_xlabel('予測誤差')
    axes[1, 2].set_ylabel('密度')
    axes[1, 2].set_title('予測誤差分布')
    
    # 正規分布をフィット
    mu, sigma = residuals.mean(), residuals.std()
    x = np.linspace(residuals.min(), residuals.max(), 100)
    y = stats.norm.pdf(x, mu, sigma)
    axes[1, 2].plot(x, y, 'r-', linewidth=2, 
                   label=f'N({mu:.2f}, {sigma:.2f}²)')
    axes[1, 2].legend()
    axes[1, 2].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    # 性能評価
    from sklearn.metrics import r2_score, mean_squared_error
    
    r2 = r2_score(test_output, predicted_test)
    mse = mean_squared_error(test_output, predicted_test)
    rmse = np.sqrt(mse)
    
    print("\n性能評価:")
    print(f"  R²スコア: {r2:.6f}")
    print(f"  MSE: {mse:.6f}")
    print(f"  RMSE: {rmse:.6f}")
    
    # Z-Scoreパラメータ
    print("\nZ-Score正規化パラメータ:")
    print(f"  入力X: μ={input_normalizer.average[0]:.6f}, σ={input_normalizer.deviation[0]:.6f}")
    print(f"  出力y: μ={output_normalizer.average[0]:.6f}, σ={output_normalizer.deviation[0]:.6f}")
    
    # 数学的検証
    print("\n数学的検証:")
    print(f"  正規化後Xの平均値: {train_input_norm.mean():.10f} (0であるべき)")
    print(f"  正規化後Xの標準偏差: {train_input_norm.std():.10f} (1であるべき)")
    print(f"  正規化後yの平均値: {train_output_norm.mean():.10f} (0であるべき)")
    print(f"  正規化後yの標準偏差: {train_output_norm.std():.10f} (1であるべき)")

neural_network_example()

七、Z-Score正規化の長所と短所

7.1 長所

  1. 分布形状の保持:データの分布形状を変更しない
  2. 外れ値に対して比較的頑健:外れ値が平均値と標準偏差に与える影響は限定的
  3. 境界制限がない:無限に増大する可能性のあるデータに適用可能
  4. 相関性の保持:特徴量間の相関係数を変更しない
  5. 正規分布データに適合:正規分布を標準正規分布に変換できる

7.2 短所

  1. 近似正規分布を要求:非正規分布データに対する効果は限定的
  2. 外れ値の影響を受ける:外れ値は平均値と標準偏差に影響を与える
  3. 有界出力を保証しない:結果が[-3, 3]範囲外になる可能性がある
  4. パラメータの保存が必要:予測時に元の平均値と標準偏差が必要

7.3 数学的形式による外れ値の影響分析

データセット \(X = \{x_1, x_2, ..., x_n\}\) を考える。平均値は \(\mu\)、標準偏差は \(\sigma\) とします。

外れ値 \(x_{n+1} = k\)(\(k\) は非常に大きい)を追加すると、新しい平均値と標準偏差は:

[\mu' = \frac{n\mu + k}{n+1} \approx \mu + \frac{k}{n} \quad (\text{nが大きい場合}) ][\sigma' = \sqrt{\frac{n}{n+1}\sigma^2 + \frac{(k - \mu')^2}{n+1}} \approx \sqrt{\sigma^2 + \frac{k^2}{n}} \quad (\text{kが大きい場合}) ]外れ値が標準偏差に与える影響が、平均値に与える影響よりも大きいことがわかります。

タグ: 正規化 Z-Score 標準化 統計処理 データ前処理

6月25日 21:55 投稿