PythonでL1/L2正規化(ノルム)を実現するScikit-learn Normalizerの使い方まとめ

Python

機械学習のモデル精度を高める上で欠かせない「データ前処理」。その中でも、特徴量のスケール(尺度)を揃えるスケーリングは非常に重要です。

Scikit-learn(サイキットラーン)には多くのスケーリング手法が用意されていますが、Normalizer(ノーマライザー)は、他のスケーラーとは少し異なるユニークな役割を持っています。

この記事では、PythonのScikit-learnライブラリを使ってNormalizerを使いこなす方法に焦点を当てます。

  • Normalizerが具体的に何をしているのか?
  • StandardScalerと何が違うのか?(StandardScalerに関する詳しい記事はこちら)
  • L1正規化とL2正規化の違いと使い分けは?

といった疑問を、具体的なPythonコード例と共に、初心者から中級者の方にも分かりやすく解説していきます。この記事を読めば、Normalizerを使ったサンプル(行)ごとの正規化をマスターできるはずです。

本記事で解説するScikit-learn Normalizerの公式ドキュメントはこちらです。

データ前処理に関するユーザーガイド(公式)はこちらです。

はじめに:Scikit-learnのNormalizerとは?

まずはNormalizerがどのようなものなのか、その基本的な役割と必要性、そして最も混同されがちなStandardScalerとの違いを明確にしましょう。

Normalizerの役割:サンプル(行)ごとのデータを正規化する

Normalizerの役割は、**「サンプル(行)ごと」**にベクトルの「大きさ」を揃えることです。

具体的には、データセットの各行(各サンプル)を、その行が持つL1ノルムまたはL2ノルム(ベクトルの長さや大きさの尺度)が1になるように変換します。

例えば、[3, 4] というデータ(行)があった場合、L2ノルム(ユークリッド距離)は $\sqrt{3^2 + 4^2} = 5$ です。Normalizer(norm='l2') は、この行の各要素を $5$ で割り、[3/5, 4/5] すなわち [0.6, 0.8] に変換します。この変換後のベクトルのL2ノルムは $\sqrt{0.6^2 + 0.8^2} = \sqrt{0.36 + 0.64} = \sqrt{1.0} = 1$ となります。

このように、各サンプルの「大きさ」を $1$ に統一するのがNormalizerの仕事です。

なぜNormalizerによる前処理が必要なのか?

Normalizerによる前処理が必要なのは、特徴量の「絶対的な大きさ」ではなく、「方向」や「パターン」に注目したい場合です。

例えば、テキストデータを扱う機械学習(自然言語処理)の分野でよく使われます。TF-IDFなどで単語の出現頻度をベクトル化した際、文書の長さ(単語数)が異なると、ベクトルの大きさもバラバラになってしまいます。

  • 短い文書:「Python」が1回
  • 長い文書:「Python」が5回

このままでは、「Python」という単語の重要性が、文書の長さによって左右されてしまいます。

Normalizerを使って各文書ベクトルを正規化(大きさを1に揃える)することで、文書の長さに関わらず、単語の構成比率(=ベクトルの方向)で比較できるようになります。

コサイン類似度のように、ベクトルの「方向」の近さを計算する手法を用いる場合、Normalizerによる前処理は特に有効です。

NormalizerとStandardScaler(標準化)との決定的な違い

NormalizerStandardScaler(標準化)は、名前は似ていますが、処理する「向き」が全く異なります。これが最も重要な違いです。

  • Normalizer(正規化): 行(サンプル)ごとに処理します。各サンプルの特徴量間の関係性(比率)を保ったまま、そのサンプルのノルムを1にします。
  • StandardScaler(標準化): 列(特徴量)ごとに処理します。各特徴量(全サンプル)の平均が0、分散が1になるように変換します。

以下の図のようなイメージです。

  • Normalizer: $\to [x_1, x_2, x_3]$ (行を見る)
  • StandardScaler: $\downarrow$ (列を見る)

データセット全体を見て「この特徴量A(列)は平均XX、分散YYだから…」と処理するのがStandardScaler。

それに対して、「このサンプル1(行)はベクトル長がXXだから…」「このサンプル2(行)はベクトル長がYYだから…」と、サンプル(行)ごとに独立して処理するのがNormalizerです。

どちらも「スケーリング」手法ですが、目的と処理単位が根本的に異なることを理解しておきましょう。

Normalizerの基本的な使い方(L2正規化)

Normalizerのデフォルトの動作は「L2正規化」です。これは最も一般的に使われるノルムで、ベクトルのユークリッド距離(原点からのまっすぐな距離)に基づいています。

早速、Pythonコードで使い方を見ていきましょう。

ライブラリのインポートとサンプルデータの準備

まず、必要なライブラリ(Normalizernumpy)をインポートし、サンプルデータを作成します。

import numpy as np
from sklearn.preprocessing import Normalizer

# サンプルデータを作成
# 3サンプル, 4特徴量
X = np.array([
    [1, 1, 2, 4],  # サンプル1
    [3, 0, 0, 5],  # サンプル2
    [0, 6, 8, 0]   # サンプル3
], dtype=np.float64) # 計算のために浮動小数点数型にしておきます

print("元のデータ (X):")
print(X)

Normalizerインスタンスの作成 (デフォルトはL2)

次に、Normalizerのインスタンスを作成します。引数を指定しない場合、自動的に norm='l2' が選択されます。

# Normalizerのインスタンスを作成
# デフォルトは norm='l2'
normalizer_l2 = Normalizer()

# 以下と同じ意味
# normalizer_l2 = Normalizer(norm='l2')

fit_transform()メソッドで正規化を実行する

作成したインスタンスの fit_transform() メソッドを使って、データを正規化します。

NormalizerStandardScalerとは異なり、fit()メソッドではデータを「学習」しません(パイプライン互換性のために形式的に存在するだけです)。transform()(またはfit_transform)が呼ばれた時点で、入力されたデータの各行を独立して正規化します。

# L2正規化を実行
X_normalized_l2 = normalizer_l2.fit_transform(X)

print("\nL2正規化後のデータ (X_normalized_l2):")
print(X_normalized_l2)

実行すると、以下のような結果が得られます(表示桁数は環境によります)。

L2正規化後のデータ (X_normalized_l2):
[[0.21821789 0.21821789 0.43643578 0.87287156]
 [0.51449576 0.         0.         0.85749293]
 [0.         0.6        0.8        0.        ]]

PythonコードでL2正規化の計算結果を確認する

L2正規化は、各行の要素の「二乗和の平方根(L2ノルム)」で、その行の各要素を割ることで実行されます。

$$\text{L2ノルム} = \sqrt{x_1^2 + x_2^2 + \dots + x_n^2}$$

手計算で確認してみましょう。

  • サンプル1 [1, 1, 2, 4]:
    • L2ノルム: $\sqrt{1^2 + 1^2 + 2^2 + 4^2} = \sqrt{1 + 1 + 4 + 16} = \sqrt{22} \approx 4.5826$
    • 正規化後:
      • $1 / \sqrt{22} \approx 0.2182$
      • $1 / \sqrt{22} \approx 0.2182$
      • $2 / \sqrt{22} \approx 0.4364$
      • $4 / \sqrt{22} \approx 0.8728$
    • これは先ほどの実行結果と一致します。
  • サンプル3 [0, 6, 8, 0]:
    • L2ノルム: $\sqrt{0^2 + 6^2 + 8^2 + 0^2} = \sqrt{0 + 36 + 64 + 0} = \sqrt{100} = 10.0$
    • 正規化後:
      • $0 / 10.0 = 0.0$
      • $6 / 10.0 = 0.6$
      • $8 / 10.0 = 0.8$
      • $0 / 10.0 = 0.0$
    • こちらも実行結果 [0., 0.6, 0.8, 0.] と一致します。

numpyを使って、正規化後の各行のL2ノルムが本当に $1$ になっているか確認することもできます。

# L2正規化後の各行のL2ノルムを計算
row_norms_l2 = np.linalg.norm(X_normalized_l2, ord=2, axis=1)

print("\nL2正規化後の各行のL2ノルム:")
print(row_norms_l2)

実行結果:

L2正規化後の各行のL2ノルム:
[1. 1. 1.]

すべての行(サンプル)のL2ノルムが $1.0$ になっていることが確認できました。

L1正規化の使い方 (norm=’l1′ を指定)

次に、もう一つの主要なノルムである「L1正規化」を見ていきましょう。L1正規化は、各行の要素の「絶対値の合計(L1ノルム)」が $1$ になるように変換します。

NormalizerでL1正規化を指定する方法

L1正規化を行うのは簡単です。Normalizerのインスタンスを作成する際に norm='l1' と指定するだけです。

# L1正規化用のNormalizerインスタンスを作成
normalizer_l1 = Normalizer(norm='l1')

L1正規化の実行コード例

先ほどと同じサンプルデータ X を使って、L1正規化を実行してみます。

# L1正規化を実行
X_normalized_l1 = normalizer_l1.fit_transform(X)

print("元のデータ (X):")
print(X)

print("\nL1正規化後のデータ (X_normalized_l1):")
print(X_normalized_l1)

実行結果は以下のようになります。

元のデータ (X):
[[1. 1. 2. 4.]
 [3. 0. 0. 5.]
 [0. 6. 8. 0.]]

L1正規化後のデータ (X_normalized_l1):
[[0.125      0.125      0.25       0.5       ]
 [0.375      0.         0.         0.625     ]
 [0.         0.42857143 0.57142857 0.        ]]

PythonコードでL1正規化の計算結果を確認する

L1正規化は、各行の要素の「絶対値の合計(L1ノルム)」で、その行の各要素を割ることで実行されます。

$$\text{L1ノルム} = |x_1| + |x_2| + \dots + |x_n|$$

  • サンプル1 [1, 1, 2, 4]: (すべて正の数なので絶対値はそのまま)
    • L1ノルム: $1 + 1 + 2 + 4 = 8$
    • 正規化後:
      • $1 / 8 = 0.125$
      • $1 / 8 = 0.125$
      • $2 / 8 = 0.25$
      • $4 / 8 = 0.5$
    • 実行結果と一致します。
  • サンプル3 [0, 6, 8, 0]:
    • L1ノルム: $0 + 6 + 8 + 0 = 14$
    • 正規化後:
      • $0 / 14 = 0.0$
      • $6 / 14 \approx 0.42857$
      • $8 / 14 \approx 0.57142$
      • $0 / 14 = 0.0$
    • こちらも実行結果と一致します。

numpyで、正規化後の各行のL1ノルム(絶対値の合計)が $1$ になっているか確認してみましょう。

# L1正規化後の各行のL1ノルムを計算 (絶対値の合計)
row_norms_l1 = np.sum(np.abs(X_normalized_l1), axis=1)

print("\nL1正規化後の各行のL1ノルム (絶対値の合計):")
print(row_norms_l1)

実行結果:

L1正規化後の各行のL1ノルム (絶対値の合計):
[1. 1. 1.]

こちらも、すべての行のL1ノルムが $1.0$ になっていることが確認できました。

L1正規化とL2正規化の違いと使い分け

L1とL2、どちらもベクトルの「大きさ」を $1$ に揃える処理ですが、その計算方法と特性が異なります。どちらを選べば良いのでしょうか?

計算方法の違い:L1ノルム(絶対値の合計)

L1ノルムは「マンハッタン距離」とも呼ばれます。各要素の絶対値を単純に合計した値です。

$$\text{L1ノルム} = \sum_{i} |x_i|$$

L1正規化は、外れ値(極端に大きな値)の影響を比較的受けにくいという特徴があります。また、結果がスパース(疎:0が多くなる)な特徴量と相性が良いとされることがあり、テキストマイニングなどでL1正規化が選ばれることもあります。

計算方法の違い:L2ノルム(ユークリッド距離)

L2ノルムは「ユークリッド距離」とも呼ばれ、私たちが普段イメージする「原点からの直線距離」です。各要素の二乗和の平方根です。

$$\text{L2ノルム} = \sqrt{\sum_{i} x_i^2}$$

L2正規化は、L1に比べて外れ値の影響をより大きく受けます(二乗するため)。しかし、数学的に扱いやすく、ベクトルの幾何学的な「長さ」を直感的に表現するため、最も一般的に使用されるノルムです。NormalizerのデフォルトもL2です。

どちらを選ぶべきか?使い分けの目安

どちらを使うべきか迷った場合の、簡単な目安を示します。

  • L2正規化 (デフォルト):
    • 迷ったらまずこちらを試す
    • コサイン類似度を計算する前処理など、ベクトルの「方向」や「角度」が重要な場合に広く使われます。
    • 一般的な機械学習タスクでまず選択されることが多いです。
  • L1正規化:
    • データに外れ値が多く、その影響を抑えたい場合。
    • 特徴量がスパース(0が多い)なデータ(例:テキストデータのBag-of-WordsやTF-IDF)を扱う場合。

多くの場合、L2正規化(デフォルト)で十分な性能が得られます。もしL2で期待した結果が得られない場合に、L1を試してみる、というアプローチが良いでしょう。

Normalizerの便利な使い方と注意点

最後に、Normalizerを使う上での便利な使い方と、特に注意すべき点について解説します。

transform()メソッド:学習済みモデルで新しいデータを変換する

StandardScalerなどの他のスケーラーでは、fit(X_train)で訓練データの平均・分散を「学習」し、その学習結果を使ってtransform(X_test)でテストデータを変換するのが一般的です。

しかしNormalizerは、前述の通りfit()では何も学習しません。

Normalizerにおけるfit()の主な役割は、sklearn.pipeline.Pipelineなどの仕組みの中で、他の中間処理(StandardScalerなど)とAPIの形式(fitしてtransformする)を統一するために存在しています。

Normalizerにおいては、fit_transform(X) を実行することと、transform(X) を実行することは、実質的に同じ結果(Xの各行を正規化する)になります。

# 新しいデータ
X_new = np.array([[10, 20, 0]], dtype=np.float64)

# L2正規化 (fitしていないnormalizer_l2インスタンスを使用)
X_new_transformed = normalizer_l2.transform(X_new)

print("新しいデータのL2正規化結果:")
print(X_new_transformed) 

# ノルムの確認 (sqrt(10^2 + 20^2) = sqrt(100 + 400) = sqrt(500))
# 10 / sqrt(500) approx 0.4472
# 20 / sqrt(500) approx 0.8944

実行結果:

新しいデータのL2正規化結果:
[[0.4472136  0.89442719 0.        ]]

このように、fit()で使った元のデータXの情報は一切使われず、transform()に渡されたX_new自体のノルムに基づいて正規化が実行されます。

fit()とtransform()を分割して実行する意味

上記の特性から、Normalizerにおいてfit(X_train)transform(X_test)を分割して実行する使い方は、StandardScalerとは根本的に意味が異なります。

  • StandardScaler.fit(X_train).transform(X_test):
    • X_train(訓練データ)の平均・分散を使って、X_test(テストデータ)を標準化する。
  • Normalizer.fit(X_train).transform(X_test):
    • fit(X_train)では何も学習しない(selfを返すだけ)。
    • transform(X_test)は、X_test(テストデータ)の各行のノルムを使って、X_testを正規化する。
    • X_trainの情報は一切使用されません。

この動作の違いは、Scikit-learnのパイプラインを組む上で非常に重要なので、必ず覚えておいてください。

注意点:Normalizerは「行」単位で処理する

この記事で何度も強調してきましたが、最後にもう一度確認します。

Normalizerは**「行」単位(サンプルごと)**で処理します。

機械学習の前処理では、StandardScalerMinMaxScalerのように「列」単位(特徴量ごと)で処理するスケーラーを使う場面の方が多いため、混同しやすいです。

  • 列(特徴量)のスケールを揃えたい:
    • StandardScaler (平均0, 分散1)
    • MinMaxScaler (最小0, 最大1)
  • 行(サンプル)のノルム(大きさ)を1に揃えたい:
    • Normalizer (L1 or L2)

自分がやりたい前処理が「列」単位なのか「行」単位なのかを明確にし、適切なスケーラーを選択するようにしてください。

まとめ:Normalizerを使いこなしデータ前処理の精度を上げよう

今回は、Scikit-learnのNormalizerについて、その基本的な使い方からL1/L2正規化の違い、StandardScalerとの決定的な違いまでを詳しく解説しました。

  • Normalizerは、行(サンプル)ごとに処理し、各行のノルム(L1またはL2)を1にします。
  • StandardScalerは、列(特徴量)ごとに処理し、各列の平均を0、分散を1にします。
  • L2正規化 (norm='l2') はデフォルトで、ベクトルの「ユークリッド距離」を1にします。迷ったらまずL2を使いましょう。
  • L1正規化 (norm='l1') は、ベクトルの「絶対値の合計」を1にします。外れ値に強い、スパースなデータで使われることがあります。
  • Normalizerfit()メソッドは何も学習せず、transform()は入力されたデータ自体のノルムで正規化を実行します。

Normalizerは、特にテキストデータや、ベクトルの「方向」が重要なタスク(コサイン類似度など)で真価を発揮するスケーラーです。

他のスケーラーとの違いを正しく理解し、適切な場面でNormalizerを使いこなして、あなたの機械学習モデルの精度向上に役立ててください。

コメント

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