辞書学習(DictionaryLearning)とは?Scikit-learnで学ぶ原理とPCAとの違い

Python

はじめに:この記事でわかること

「辞書学習(Dictionary Learning)」という言葉を聞いたことがありますか?

機械学習、特に信号処理や画像処理の分野で非常に強力な手法でありながら、主成分分析(PCA)ほどは知られていないかもしれません。「名前は聞いたことがあるけど、何をやっているのかよくわからない」「PCAと何が違うの?」と感じている方も多いのではないでしょうか。

この記事は、まさにそんなあなたのために書かれました。

この記事を最後まで読むことで、あなたは以下の知識を手に入れることができます。

  • 辞書学習(Dictionary Learning)の基本的な概念がわかる
  • なぜ辞書学習が重要なのかが理解できる
  • Scikit-learnを使った具体的な実装方法が身につく
  • 主成分分析(PCA)との違いを通して、使い分けがイメージできる

データの中から本質的な「特徴」を抽出し、ノイズ除去やデータ圧縮などに応用できる辞書学習の世界を、Pythonのコードを動かしながら一緒に探検していきましょう。

辞書学習(Dictionary Learning)の核心に迫る

まずは、辞書学習の最も重要な考え方について、できるだけ直感的に理解できるように解説します。

辞書学習とは、一言でいうと「データを表現するための最適な『辞書』を作る技術」

結論から言うと、辞書学習は、手元にあるデータ(例えば、大量の画像)を最も効率的に表現できるような「辞書」と、その「辞書」の最適な使い方を同時に学習する手法です。

ここで言う「辞書」とは、国語辞典のようなものではありません。 データの世界における辞書とは、そのデータを構成するための基本的な部品(要素)の集合だと考えてください。

例えば、人間の顔画像を考えてみましょう。 顔画像を構成する部品には、「目」「鼻」「口」「輪郭」などがあります。もし、これらの部品が完璧に揃った「顔辞書」があれば、どんな人の顔でも「この辞書の『目』パーツと『鼻』パーツをこう組み合わせて…」という形で表現できるはずです。

辞書学習は、このような「最適な部品(辞書)」を、コンピュータがデータから自動的に見つけ出すための技術なのです。

「辞書(Dictionary)」と「スパースな表現(Sparse Code)」とは何か?

辞書学習を理解する上で欠かせないのが、「辞書」と「スパースな表現」という2つのキーワードです。

辞書:データの特徴を捉えた基本的な要素(基底アトム)の集まり

先ほどの例で言えば、「目」や「鼻」といった部品が辞書の要素にあたります。これらを専門用語で基底アトム (Base Atom) や単にアトムと呼びます。そして、これらのアトムを集めたものが辞書 (Dictionary) です。

辞書学習が優れているのは、この辞書を人間が設計するのではなく、データそのものから学習して生成する点にあります。例えば、手書き数字の画像データセットを学習させれば、数字を構成する「直線」や「曲線」といった要素が自動的に辞書のアトムとして抽出されます。

これにより、そのデータドメインに特化した、非常に表現力の高い辞書を獲得することができるのです。

スパースな表現:ごく少数の辞書要素の組み合わせで元のデータを表現すること

「スパース(Sparse)」とは、日本語で「疎(そ)」や「まばら」という意味です。 スパースな表現(スパースコーディング)とは、たくさんの辞書アトムの中から、ごく一部のアトムだけを使って元のデータを表現することを目指します。

例えば、1000個のアトムを持つ辞書があったとします。あるデータを表現するのに、1000個すべてのアトムを少しずつ使うのではなく、たった5個のアトムの組み合わせだけで表現しよう、というのがスパースな表現の考え方です。

この「なるべく少ない要素で表現する」という制約が、辞書学習において非常に重要な役割を果たします。

なぜ「スパース(疎)」であることが重要なのか?

では、なぜわざわざ「スパース」な表現にこだわるのでしょうか?それには、大きく分けて3つのメリットがあります。

  1. 本質的な特徴だけを抽出できる スパースな表現を強制することで、データにとって本当に重要な、本質的な特徴だけが抽出されやすくなります。多くの無関係な情報(ノイズなど)は無視され、データの本質を捉えた少数のアトムだけが選択されるからです。これにより、モデルの解釈性が向上します。
  2. ノイズに強い表現を獲得できる データに含まれるランダムなノイズは、特定のパターンを持っていません。そのため、辞書学習で獲得した「意味のあるパターン」を持つアトムではうまく表現することができません。結果として、スパースな表現を求める過程でノイズ成分が自然と分離され、ノイズに強い頑健なデータ表現を得ることができます。これは後ほど実践する画像ノイズ除去(デノイジング)で強力な効果を発揮します。
  3. データの圧縮につながる 元のデータを表現するのに必要な情報が「どの辞書アトムを」「どれくらいの強さで」使うか、という情報だけになります。これがごく少数で済むため、結果的にデータの圧縮にも繋がります。

このように、辞書学習は「データに特化した辞書」と「スパースな表現」を同時に求めることで、データの本質を捉える強力な手法なのです。

主成分分析(PCA)と辞書学習は何が違うのか?

辞書学習としばしば比較される手法に、主成分分析(PCA: Principal Component Analysis) があります。どちらもデータの特徴を捉えるための手法ですが、そのアプローチには根本的な違いがあります。

結論:PCAは「分散が最大になる軸」を探すのに対し、辞書学習は「スパースに表現できる辞書」を探す

これが両者の最も本質的な違いです。 PCAの目的は、主に次元削減です。データの情報をできるだけ失わずに、より低い次元の空間にデータを写し取ることを目指します。

一方、辞書学習の主な目的は、スパースな表現の獲得です。次元を削減すること自体が目的ではなく、データの本質的な構成要素(アトム)を見つけ出し、それらの少数の組み合わせでデータを再構成することを目指します。

PCAの考え方:データのばらつきを最もよく表す直交した軸(主成分)を見つける

PCAは、データが最もばらついている方向(分散が最大になる方向)を第1主成分、次にその方向と直交する方向で最もばらついている方向を第2主成分…というように、互いに直交する軸(主成分) を見つけ出します。

この主成分は、言わばデータを表現するための「新しい座標軸」です。そして、これらの軸は必ず互いに直交している(相関がない)という強い制約があります。これは、データの共分散行列の固有値問題を解くことで求められます。

PCAは、この直交する軸(基底)を用いてデータを表現しようとします。

辞書学習の考え方:辞書の要素(基底アトム)は直交するとは限らない(過完備)

辞書学習で見つけ出す辞書のアトム(基底)は、PCAの主成分とは異なり、必ずしも互いに直交している必要はありません

むしろ、より多くの種類の特徴的なアトムを辞書に含めることで、データを柔軟に表現しようとします。そのため、アトムの数を元のデータの次元数よりも多くすることも一般的です。これを過完備 (Overcomplete) な辞書と呼びます。

例えば、画像データにおいて「斜め45度の線」と「斜め50度の線」は、互いに似ていますが(直交しない)、両方を辞書に含めておくことで、より繊細な表現が可能になります。PCAでは、直交性の制約からこのような柔軟な基底を持つことはできません。

どう使い分ける?ユースケースで比較

観点主成分分析 (PCA)辞書学習 (Dictionary Learning)
主目的次元削減、可視化スパースな表現の獲得、特徴抽出
基底(軸/アトム)互いに直交する必ずしも直交しない(過完備も可能)
表現方法全ての主成分の線形結合で表現ごく少数の辞書アトムの線形結合で表現
計算コスト比較的低い(固有値分解)比較的高くなる傾向がある(最適化計算)
得意なタスク・データの全体的な構造把握 ・特徴量の次元を減らしたい時 ・ノイズの少ないデータの可視化・画像/音声のノイズ除去 ・画像の修復(インペインティング) ・信号の圧縮 ・データから本質的な構成要素を抽出したい時

単純にデータの次元を削減して計算量を減らしたい、あるいはデータを2次元や3次元にプロットして可視化したい、という場合はPCAが適しています。

一方で、データに含まれるノイズを除去したい、画像の一部が欠損したのを修復したい、あるいはデータが「何からできているのか」という構成要素そのものに興味がある場合は、辞書学習が非常に強力な選択肢となります。

Scikit-learnでDictionaryLearningを動かしてみよう

それでは、いよいよPythonのライブラリScikit-learnを使って、辞書学習を実際に動かしてみましょう。 ここでは、画像にノイズを加え、辞書学習によってそのノイズがきれいに除去される様子を確認します。

本記事のコードは、scikit-learnのバージョン1.0以降を想定しています。

環境構築とライブラリのインポート

まずは、必要なライブラリをインストールします。まだの方は、ターミナルやコマンドプロンプトで以下のコマンドを実行してください。

pip install scikit-learn numpy matplotlib

インストールが完了したら、PythonスクリプトやJupyter Notebookで必要なライブラリをインポートします。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import DictionaryLearning
from sklearn.feature_extraction.image import extract_patches_2d, reconstruct_from_patches_2d
from skimage.data import camera
from skimage.util import random_noise

print("ライブラリのインポートが完了しました。")

サンプルデータの準備:ノイズを含んだ画像を生成する

今回は、scikit-imageライブラリに含まれている有名な白黒画像「camera」を使用します。この画像に人工的にノイズを加えて、辞書学習の実験台となるデータを作成します。

# オリジナルの画像を読み込む
original_image = camera()

# 画像を0から1の範囲に正規化
original_image = original_image / 255.0

# 画像にガウシアンノイズを追加する
# np.random.seedで乱数を固定し、毎回同じ結果になるようにする
np.random.seed(0)
noisy_image = random_noise(original_image, mode='gaussian', var=0.01)

# オリジナル画像とノイズ付き画像を表示して比較
fig, axes = plt.subplots(1, 2, figsize=(10, 5))

axes[0].imshow(original_image, cmap='gray')
axes[0].set_title('Original Image')
axes[0].axis('off')

axes[1].imshow(noisy_image, cmap='gray')
axes[1].set_title('Noisy Image')
axes[1].axis('off')

plt.tight_layout()
plt.show()

print(f"画像の形状: {original_image.shape}")

実行すると、左側に元のきれいな画像、右側にザラザラとしたノイズが乗った画像が表示されるはずです。我々の目標は、この右側のノイズ付き画像から、左側のオリジナル画像に近い状態を復元することです。

DictionaryLearningクラスの基本的な使い方

Scikit-learnでは、sklearn.decomposition.DictionaryLearningクラスを使うことで簡単に辞書学習を実装できます。

画像を小さなパッチ(部分領域)に分割し、そのパッチの集合から「画像の部品」となる辞書を学習させます。

# --- パラメータ設定 ---
# パッチのサイズ (縦x横)
patch_size = (7, 7)
# 辞書のアトムの数(元のパッチの次元数より多く設定し、過完備辞書とする)
n_components = 100
# スパース性を制御する正則化パラメータ (大きいほどスパースになる)
alpha = 0.8
# 辞書学習の繰り返し回数
n_iter = 500
# スパースコーディングに使用するアルゴリズム
transform_algorithm = 'omp' # or 'lars'

# ノイズ付き画像からパッチを抽出
print("ノイズ付き画像からパッチを抽出しています...")
data = extract_patches_2d(noisy_image, patch_size)
# 抽出したパッチを1次元のベクトルに変換
data = data.reshape(data.shape[0], -1)
print(f"抽出されたパッチの数: {data.shape[0]}, 各パッチの次元数: {data.shape[1]}")

# 辞書学習モデルのインスタンス化
print("DictionaryLearningモデルを初期化しています...")
dico = DictionaryLearning(
    n_components=n_components,
    alpha=alpha,
    n_iter=n_iter,
    random_state=0,
    transform_algorithm=transform_algorithm
)

# パッチデータを使って辞書の学習を実行
# この処理には少し時間がかかります
print("辞書の学習を開始します...")
dico.fit(data)
print("辞書の学習が完了しました。")

ここで設定した主要なパラメータの意味を解説します。

  • n_components: 辞書に含まれるアトム(基底)の数です。パッチの次元数 (7*7=49) よりも多い100に設定することで、過完備な辞書を学習させます。
  • alpha: スパース性を制御する正則化項の強さです。この値が大きいほど、より少ないアトムでデータを表現しようとする(よりスパースになる)力が強くなります。
  • n_iter: 学習の繰り返し回数です。
  • transform_algorithm: スパースな表現を求めるためのアルゴリズムを指定します。'omp' (Orthogonal Matching Pursuit) や 'lars' (Least Angle Regression) がよく使われます。OMPの方が高速な傾向があります。

学習した「辞書」を可視化してみる

学習が完了すると、モデルのcomponents_属性に学習された辞書が格納されます。これは、1つ1つのアトムが1次元のベクトルとして表現されたものです。これをパッチの形状に戻して可視化することで、どのような「部品」が学習されたかを見ることができます。

# 学習した辞書(アトムの集合)を取得
dictionary = dico.components_

# 辞書を可視化
plt.figure(figsize=(10, 10))
for i, atom in enumerate(dictionary):
    plt.subplot(10, 10, i + 1)
    # 1次元のベクトルを元のパッチの形状に戻す
    plt.imshow(atom.reshape(patch_size), cmap='gray')
    plt.axis('off')

plt.suptitle(f'Learned Dictionary ({n_components} atoms)', fontsize=16)
plt.show()

実行すると、100個の小さな画像パッチが表示されます。よく見ると、様々な方向の「線」や「エッジ」、「グラデーション」のような、画像を構成する基本的なパターンが学習されていることがわかります。これらが、我々のデータから自動的に抽出された「最適な部品」なのです。

辞書を使ってノイズ除去を実践

最後に、学習した辞書を使ってノイズ付き画像のノイズ除去(デノイジング)を行います。

処理の流れは以下の通りです。

  1. ノイズ付き画像の各パッチに対して、学習した辞書を使ってスパースな表現を求めます(transformメソッド)。
  2. このスパースな表現と辞書を使って、パッチを再構成します。このとき、ノイズ成分はうまく表現できないため、スパース表現には含まれにくくなります。
  3. 再構成されたクリーンなパッチを元の画像の位置に戻して、最終的なノイズ除去済み画像を生成します。
# ノイズ付き画像のパッチデータに対してスパース表現を計算
print("スパース表現を計算しています...")
code = dico.transform(data)

# スパース表現と辞書からパッチを再構成
print("パッチを再構成しています...")
patches_reconstructed = np.dot(code, dictionary)
patches_reconstructed = patches_reconstructed.reshape(len(data), *patch_size)

# 再構成したパッチを元の画像に戻す
print("画像を再構成しています...")
image_reconstructed = reconstruct_from_patches_2d(patches_reconstructed, original_image.shape)
print("ノイズ除去が完了しました。")

# 結果を比較表示
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(noisy_image, cmap='gray')
axes[0].set_title('Noisy Image')
axes[0].axis('off')

axes[1].imshow(image_reconstructed, cmap='gray')
axes[1].set_title('Denoised Image')
axes[1].axis('off')

axes[2].imshow(original_image, cmap='gray')
axes[2].set_title('Original Image')
axes[2].axis('off')

plt.tight_layout()
plt.show()

# PSNR (Peak Signal-to-Noise Ratio) を計算して品質を評価する(オプション)
def psnr(img1, img2):
    mse = np.mean((img1 - img2) ** 2)
    if mse == 0:
        return 100
    PIXEL_MAX = 1.0
    return 20 * np.log10(PIXEL_MAX / np.sqrt(mse))

psnr_noisy = psnr(original_image, noisy_image)
psnr_denoised = psnr(original_image, image_reconstructed)

print(f"PSNR (Noisy Image): {psnr_noisy:.2f} dB")
print(f"PSNR (Denoised Image): {psnr_denoised:.2f} dB")

結果を見ると、真ん中の「Denoised Image」が、左の「Noisy Image」に比べてザラザラ感が大幅に減少し、右の「Original Image」にかなり近づいていることが確認できるはずです。

画質評価指標であるPSNR(ピーク信号対雑音比)を計算してみても、値が大きいほど高画質であることを示しますが、ノイズ除去後の画像の方が値が大幅に改善していることが数値的にもわかります。

これは、辞書学習が画像の構造的な特徴(線やエッジなど)をうまく捉え、それらで表現できないランダムなノイズ成分を効果的に分離・除去できたことを示しています。

まとめ:辞書学習を理解してデータ表現の引き出しを増やそう

今回は、辞書学習(Dictionary Learning)という強力な表現学習の手法について、その原理からPCAとの違い、そしてScikit-learnを用いた具体的な実装までを詳しく解説しました。

最後に、この記事の要点を振り返りましょう。

  • 辞書学習は、データをスパースに(ごく少数の要素で)表現する「最適な辞書」をデータ自身から獲得する手法です。
  • PCAの基底が互いに直交するのに対し、辞書学習の基底(アトム)は非直交過完備にできるため、より柔軟で表現力の高い特徴を獲得できます。
  • この特性を活かし、画像や音声のノイズ除去、欠損データの修復(インペインティング)、特徴抽出など、幅広いタスクに応用されています。
  • Scikit-learnライブラリを使えば、DictionaryLearningクラスを用いて、数行のコードで辞書学習を実装し、その効果を試すことができます。

辞書学習は、単なる次元削減にとどまらない、データの本質的な構造を捉えるための非常に奥深いアプローチです。この手法を理解することで、あなたのデータ分析や機械学習モデル構築の「引き出し」が一つ増えたことは間違いありません。

ぜひ、ご自身のデータセットでもこの強力なツールを試してみてください。

コメント

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