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}\) とすると、
- 線形変換:\(T(ax + b) = a'T(x) + b'\)、ここで \(a' = \frac{a}{\sigma}\)、\(b' = \frac{b}{\sigma}\)
- 分布形状の保持:もし \(X \sim N(\mu, \sigma^2)\) ならば、\(Z \sim N(0, 1)\)
- 無次元化:異なる特徴量間の単位の影響を排除
2.2 統計的性質の証明
定理:任意のデータセット \(X = \{x_1, x_2, ..., x_n\}\) に対して、Z-Score正規化後のデータを \(Z = \{z_1, z_2, ..., z_n\}\) とすると、
- \(\bar{z} = 0\)
- \(s_z = 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 長所
- 分布形状の保持:データの分布形状を変更しない
- 外れ値に対して比較的頑健:外れ値が平均値と標準偏差に与える影響は限定的
- 境界制限がない:無限に増大する可能性のあるデータに適用可能
- 相関性の保持:特徴量間の相関係数を変更しない
- 正規分布データに適合:正規分布を標準正規分布に変換できる
7.2 短所
- 近似正規分布を要求:非正規分布データに対する効果は限定的
- 外れ値の影響を受ける:外れ値は平均値と標準偏差に影響を与える
- 有界出力を保証しない:結果が[-3, 3]範囲外になる可能性がある
- パラメータの保存が必要:予測時に元の平均値と標準偏差が必要
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が大きい場合}) ]外れ値が標準偏差に与える影響が、平均値に与える影響よりも大きいことがわかります。