Back to Blog
By AI Language Model (Gemini 2.5 Pro)

機械学習の原点 完全ガイド:パーセプトロンとADALINEから学ぶAIの基礎原理とPython実装

AIと深層学習の根幹にあるパーセプトロンとADALINE。この記事では、その歴史的背景、シンプルな数理モデル、学習の仕組み、そして限界と発展を、初学者にも明快に、かつ深く理解できるよう、Pythonコードと図解を交えて徹底的に解説します。

機械学習の原点 完全ガイド:パーセプトロンとADALINEから学ぶAIの基礎原理とPython実装

導入:なぜ今、半世紀前のアルゴリズムを学ぶのか? 未来への羅針盤としての古典

私たちは今、人工知能(AI)が社会の風景を一変させつつある時代に生きています。特に**深層学習(Deep Learning)**と呼ばれる技術は、画像認識、自然言語処理、自動運転など、SFの世界を現実のものとしつつあります。これらの華々しい成果を目にすると、AIはまるで魔法のように感じられるかもしれません。

しかし、どんなに複雑に見える魔法にも、その根底にはシンプルで強力な原理が存在します。現代AIの目覚ましい進歩を支える技術の多くは、その基礎を、コンピュータサイエンスの黎明期、1950年代から60年代にかけて生まれた、驚くほどエレガントなアルゴリズムに置いています。本稿で徹底的に解剖する**パーセプトロン(Perceptron)ADALINE(Adaptive Linear Neuron)**は、まさにその代表格であり、現代ニューラルネットワークの直接的な祖先にあたる存在です。

「なぜ、日進月歩のAI分野で、わざわざ半世紀以上も前の古いアルゴリズムを学ぶ必要があるのか?」――そう疑問に思う方もいるでしょう。答えは、本質的な理解にあります。最新技術の表面だけを追いかけても、その真の力や限界、そして未来の可能性を見通すことはできません。パーセプトロンやADALINEの仕組み、その数理的な裏付け、そして何ができて何ができなかったのかを深く理解することは、複雑な現代AIを「得体の知れないブラックボックス」としてではなく、その内部構造と基本原理を理解した「透明なホワイトボックス」として捉えるための、いわば知的な解剖学の基礎を身につけることに他なりません。

本稿の対象読者と目標

この記事は、AIや機械学習の世界に足を踏み入れたばかりの初学者の方々、あるいは基礎から体系的に学び直したいと考えている方々を主な対象としています。専門的な知識は前提としませんが、新しい概念を学ぶ意欲と、少しの数学(高校レベルの線形代数・微分の基礎があれば尚可)やプログラミング(Pythonの基本)への興味があれば十分です。

本稿の目標は、単にアルゴリズムの概要を説明することではありません。以下の達成を目指します。

この記事のロードマップ

この知的な探求の旅は、以下の章立てで進められます。

  1. 第1章:脳に学ぶ機械 ― 人工ニューロンの誕生: AIの夢の原点と、生物の脳から着想を得た最初の計算モデルMCPニューロン。
  2. 第2章:パーセプトロン ― 「学習」する最初の機械: 誤りから学ぶ画期的なアイデア、パーセプトロンの構造と学習規則。
  3. 第3章:手を動かして理解する ― Pythonでパーセプトロンを実装: 実際にコードを書き、Irisデータセットでパーセプトロンを動かしてみる。
  4. 第4章:最適化への道筋 ― ADALINEと勾配降下法: より洗練された学習へ。目的関数と勾配降下法の導入。
  5. 第5章:ADALINEの実装と実践 ― 特徴量スケーリングの重要性: ADALINE(SGD)を実装し、実践における「罠」とその回避策を探る。
  6. 第6章:線形モデルの壁と未来 ― MLPと深層学習へ: 線形モデルの限界を乗り越え、現代AIへと至る道筋。
  7. 第7章:より深く理解するために ― 理論的背景と実践Tips: 汎化能力、モデル複雑性、そして実装上の注意点。

さあ、準備はいいですか? 機械学習の原点を探る旅に出発しましょう。この旅を通じて、AIという壮大な建造物を支える、シンプルでありながら強固な基礎原理を発見できるはずです。


第1章:脳に学ぶ機械 ― 人工ニューロンの誕生

人間のように「考える」機械を作りたい――この夢は、古くから人類を魅了してきました。20世紀に入り、アラン・チューリングらが計算の理論的可能性を探求し、電子計算機が現実のものとなると、その夢は具体的な科学技術的挑戦へと姿を変えました。特に、人間の知能の源であるの仕組みを模倣することで、知的な機械を実現しようとするアプローチが注目を集めました。

1.1 生物の脳:究極の情報処理マシン

私たちの脳は、驚異的な能力を持つ情報処理システムです。思考、記憶、学習、認識、運動制御といった複雑な機能を、膨大な数の神経細胞、すなわち**ニューロン(Neuron)**のネットワークによって実現しています。この生物学的ニューロンの基本的な構造と機能の理解が、人工ニューロンモデル開発の出発点となりました。

図1.1:典型的な生物学的ニューロンの構造。情報は主に左から右へ流れる。

ニューロンの働き(単純化版):

  1. 入力: 複数の他のニューロンから、シナプスを介して信号(興奮性または抑制性)を受け取る。
  2. 統合: 細胞体で、これらの信号が時間的・空間的に足し合わされる。
  3. 発火判定: 統合された信号の強さ(膜電位)がある**閾値(Threshold)**を超えると、ニューロンは「発火(Fire)」する。超えなければ発火しない(全か無かの法則 All-or-None Law)。
  4. 出力伝達: 発火すると、活動電位が軸索を伝わり、シナプスから次の細胞へ信号が送られる。

この、入力の統合、閾値による発火判定、そして出力の伝達という基本的なメカニズムが、初期の人工ニューロンモデルの着想源となりました。

1.2 計算モデルへの第一歩:MCPニューロン (1943年)

1943年、神経生理学者ウォーレン・マカロック(Warren McCulloch)と若き論理学者ウォルター・ピッツ(Walter Pitts)は、この生物学的ニューロンの振る舞いを、非常に単純な数学的・論理的モデルとして表現する画期的な論文を発表しました1。これがMCP(McCulloch-Pitts)ニューロンモデルです。

彼らは、ニューロンを以下のような単純な二値(0か1)の閾値ユニットとして捉えました。

graph LR
    subgraph "MCPニューロン (論理ゲートモデル)"
        direction LR
        I1(入力 x1) -- "+w1" --> S((Σ))
        I2(入力 x2) -- "+w2" --> S
        Idots(...) -- "..." --> S
        Im(入力 xm) -- "+wm" --> S
        S -- " z ≥ θ ?" --> O{"出力 y (0 or 1)"}
        style S fill:#ccf,stroke:#333,stroke-width:2px
        style O fill:#f9f,stroke:#333,stroke-width:2px
    end
    %% 抑制性入力は省略 (commented out Japanese note)

図1.2:MCPニューロンの概念図(現代的解釈を含む)。入力の重み付き和 zz が閾値 θ\theta 以上なら1、そうでなければ0を出力する単純な閾値ユニット。

MCPニューロンの意義:

1.3 MCPニューロンの限界:学習能力の不在

MCPニューロンは画期的でしたが、大きな限界も抱えていました。それは、学習能力を持たないことです。モデルのパラメータである重み wjw_j と閾値 θ\theta は、特定の論理演算や機能を実現するように、人間が事前に計算し、手動で設定しなければなりませんでした。

生物の脳が持つ最も驚くべき能力の一つは、経験を通じて学び、適応していく能力です。環境からの入力に応じて、ニューロン間の接続の強さ(シナプス強度)が変化し、ネットワーク全体の振る舞いが変わっていきます。MCPニューロンには、このようなデータから自動的にパラメータを調整するメカニズム、すなわち学習アルゴリズムが欠けていました。

この「学習」の壁を打ち破ることが、人工知能研究の次の大きな目標となりました。脳の可塑性を模倣し、経験から学ぶことができる人工ニューロンモデルはどのように構築できるのか? この問いに対する最初の、そして最も影響力のある答えの一つが、次章で詳述するパーセプトロンだったのです。


第2章:パーセプトロン ― 「学習」する最初の機械

MCPニューロンが計算の基礎を築いた後、AI研究の焦点は「どのようにして機械に学習させるか」へと移りました。この問いに対する画期的なブレークスルーをもたらしたのが、1957年にコーネル航空研究所の心理学者フランク・ローゼンブラット(Frank Rosenblatt)によって提案されたパーセプトロン(Perceptron)です2。パーセプトロンは、MCPモデルに学習規則を導入し、データから自動的にパラメータを調整する能力を持つ、最初の実用的な人工ニューロンモデルの一つとなりました。

2.1 パーセプトロンの仕組み:モデルの解剖

パーセプトロンは、基本的に**二値線形分類器(binary linear classifier)**です。与えられた入力データが、予め定義された2つのクラスのうちどちらに属するかを判定します。その構造と計算プロセスを詳しく見ていきましょう。

入力と重み: モデルは、mm個の特徴量(feature)を持つ入力ベクトル x\mathbf{x} を受け取ります。

x=[x1x2xm]Rm\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_m \end{bmatrix} \in \mathbb{R}^m

各特徴量 xjx_j には、その特徴量の重要度を示す実数値の重み(weight) wjw_j が関連付けられています。これらの重みをまとめたものが重みベクトル w\mathbf{w} です。

w=[w1w2wm]Rm\mathbf{w} = \begin{bmatrix} w_1 \\ w_2 \\ \vdots \\ w_m \end{bmatrix} \in \mathbb{R}^m

総入力(Net Input)の計算: パーセプトロンは、まず入力と重みの線形結合(linear combination)を計算します。これは総入力 zz と呼ばれ、数学的にはベクトル x\mathbf{x}w\mathbf{w} の**ドット積(内積)**として表されます。

z=w1x1+w2x2++wmxm=j=1mwjxj=wTx(2.1)z = w_1 x_1 + w_2 x_2 + \dots + w_m x_m = \sum_{j=1}^{m} w_j x_j = \mathbf{w}^T \mathbf{x} \tag{2.1}

ここで、wT\mathbf{w}^T はベクトル w\mathbf{w} の**転置(transpose)**を表し、wT=[w1,w2,,wm]\mathbf{w}^T = [w_1, w_2, \dots, w_m] という行ベクトルになります。ドット積は、対応する要素同士を掛け合わせて全て足し合わせる演算です。

補足:線形代数の基礎(転置とドット積)

  • 転置: 列ベクトル(縦長のベクトル)を行ベクトル(横長のベクトル)に、またはその逆に変換する操作です。行列の場合は、行と列を入れ替えます。AijT=AjiA_{ij}^T = A_{ji}
  • ドット積: 2つの同じ長さのベクトル a\mathbf{a}b\mathbf{b} のドット積 ab\mathbf{a} \cdot \mathbf{b} (または aTb\mathbf{a}^T \mathbf{b}) は、iaibi\sum_{i} a_i b_i で計算されます。これは、ベクトル間の「類似度」や「射影」に関連する重要な量です。パーセプトロンでは、入力が重みベクトルとどれだけ「似ている」か(同じ方向を向いているか)を測る指標として機能します。

活性化関数(Activation Function)と出力: 次に、計算された総入力 zz活性化関数 σ()\sigma(\cdot) に入力し、最終的な出力 y^\hat{y} (予測されるクラスラベル)を得ます。パーセプトロンで伝統的に使われるのは、ヘヴィサイドのステップ関数(Heaviside step function)または単にステップ関数と呼ばれるものです。

y^=σ(z;θ)={1if zθ1if z<θ(2.2)\hat{y} = \sigma(z; \theta) = \begin{cases} 1 & \text{if } z \geq \theta \\ -1 & \text{if } z < \theta \end{cases} \tag{2.2}

ここで θ\theta は**閾値(threshold)**です。総入力 zz が閾値 θ\theta 以上であればクラス1、そうでなければクラス-1(文献によってはクラス0)と予測します。これは、MCPニューロンの「全か無かの法則」を直接的にモデル化したものです。

バイアス項(Bias Term)の導入: 閾値 θ\theta を直接扱う代わりに、数式を簡略化し、モデルの柔軟性を高めるためにバイアス項 bb を導入するのが一般的です。式(2.2)の条件 zθz \ge \thetazθ0z - \theta \ge 0 と同値です。ここで、b=θb = -\theta と定義し、これを定数項として総入力の計算に加えます。

z=w1x1++wmxm+b=(j=1mwjxj)+b(2.3a)z = w_1 x_1 + \dots + w_m x_m + b = (\sum_{j=1}^{m} w_j x_j) + b \tag{2.3a}

さらに、このバイアス項 bb を、w0=bw_0 = b とし、常に入力値が x0=1x_0 = 1 である仮想的な入力特徴量を考えることで、重みベクトルの一部として統一的に扱うことができます。

x=[1x1xm],w=[bw1wm]=[w0w1wm]\mathbf{x}' = \begin{bmatrix} 1 \\ x_1 \\ \vdots \\ x_m \end{bmatrix}, \quad \mathbf{w}' = \begin{bmatrix} b \\ w_1 \\ \vdots \\ w_m \end{bmatrix} = \begin{bmatrix} w_0 \\ w_1 \\ \vdots \\ w_m \end{bmatrix}

この拡張されたベクトル x\mathbf{x}'w\mathbf{w}' を用いると、総入力 zz は単純なドット積で書けます。

z=w0x0+w1x1++wmxm=j=0mwjxj=(w)Tx(2.3b)z = w_0 x_0 + w_1 x_1 + \dots + w_m x_m = \sum_{j=0}^{m} w_j x_j = (\mathbf{w}')^T \mathbf{x}' \tag{2.3b}

これ以降、簡単のため、この拡張された重みベクトルと入力ベクトルをそれぞれ w\mathbf{w}x\mathbf{x} と表記します(文脈で区別してください)。

バイアス項を導入したことで、活性化関数の閾値は実質的に 0 となり、判定はよりシンプルになります。

y^=σ(z)={1if z=wTx01if z=wTx<0(2.4)\hat{y} = \sigma(z) = \begin{cases} 1 & \text{if } z = \mathbf{w}^T \mathbf{x} \geq 0 \\ -1 & \text{if } z = \mathbf{w}^T \mathbf{x} < 0 \end{cases} \tag{2.4}

バイアス項の幾何学的な意味

総入力 z=wTx=0z = \mathbf{w}^T \mathbf{x} = 0 は、mm次元空間内の超平面(hyperplane)を定義します。2次元なら直線、3次元なら平面です。この超平面が、パーセプトロンが予測するクラス1の領域とクラス-1の領域を分ける決定境界(decision boundary)となります。もしバイアス項 w0w_0 がなければ、この超平面は必ず原点 (0,0,,0)(0, 0, \dots, 0) を通らなければなりません。バイアス項 w0w_0 を導入することで、この超平面を原点から自由に平行移動させることが可能になり、より多くのデータ配置に対応できるようになります。重みベクトル w=[w1,,wm]T\mathbf{w} = [w_1, \dots, w_m]^T (バイアス除く) は、この決定境界に**垂直な方向(法線ベクトル)**を指し、境界の向きを決定します。

図2.1:パーセプトロンモデルの図解。入力 xjx_j は重み wjw_j で重み付けされ、合計 zz が計算される(バイアス w0w_0 も加算)。zz がステップ関数(閾値関数)σ\sigma に入力され、最終的な二値出力 y^\hat{y} が得られる。決定境界は z=0z=0 で定義される。

2.2 パーセプトロン学習規則:誤りから学ぶ知恵

パーセプトロンの真骨頂は、これらの重み w\mathbf{w} を、与えられた訓練データから自動的に学習するアルゴリズム、すなわちパーセプトロン学習規則にあります。これは教師あり学習であり、入力 x(i)\mathbf{x}^{(i)} と正解ラベル y(i){1,1}y^{(i)} \in \{1, -1\} のペアが多数与えられることを前提とします。

学習アルゴリズム:

  1. 重みの初期化: 重みベクトル w\mathbf{w} の全ての要素(バイアス w0w_0 を含む)を 0 または小さなランダムな値で初期化します。
  2. エポックの反復: 以下のステップを、予め定められた回数(エポック数)繰り返すか、あるいは訓練データ全体で誤分類がなくなるまで繰り返します。
    • 各サンプルでの学習: 訓練データセット {(x(i),y(i))i=1,,n}\{(\mathbf{x}^{(i)}, y^{(i)}) \mid i=1, \dots, n\} の各サンプル ii について、順番に以下を実行します。 a. 予測: 現在の重み w\mathbf{w} を用いて、入力 x(i)\mathbf{x}^{(i)} に対する予測ラベル y^(i)\hat{y}^{(i)} を計算します。

      y^(i)=σ(wTx(i))\hat{y}^{(i)} = \sigma(\mathbf{w}^T \mathbf{x}^{(i)})

      b. 重みの更新: 予測 y^(i)\hat{y}^{(i)} と正解ラベル y(i)y^{(i)} を比較し、もし予測が間違っていれば (y(i)y^(i)y^{(i)} \neq \hat{y}^{(i)})、重みベクトル w\mathbf{w} を以下のように更新します。

      w:=w+Δw(2.5)\mathbf{w} := \mathbf{w} + \Delta \mathbf{w} \tag{2.5}

      ここで、更新量 Δw\Delta \mathbf{w} は、

      Δw=η(y(i)y^(i))x(i)(2.6)\Delta \mathbf{w} = \eta (y^{(i)} - \hat{y}^{(i)}) \mathbf{x}^{(i)} \tag{2.6}

      η\eta は**学習率(learning rate)**と呼ばれる正の定数(通常 0<η10 < \eta \leq 1)で、更新のステップサイズを制御します。予測が正しければ (y^(i)=y(i)\hat{y}^{(i)} = y^{(i)})、誤差 y(i)y^(i)y^{(i)} - \hat{y}^{(i)} は 0 になり、Δw=0\Delta \mathbf{w} = \mathbf{0}、つまり重みは更新されません。

学習はどのように進むのか?

この単純な更新規則が、なぜ学習を可能にするのでしょうか? 予測が間違った場合の2つのケースを見てみましょう。(y(i),y^(i)y^{(i)}, \hat{y}^{(i)}{1,1}\{1, -1\})

つまり、パーセプトロン学習規則は、予測を間違えるたびに、その間違いを是正する方向に重みを微調整するという、非常に直感的で合理的なメカニズムに基づいています。

学習率 η\eta の役割: 学習率 η\eta は、この「微調整」の大きさを決めます。

図2.2:パーセプトロンの学習プロセスを図解化したもの。訓練サンプルを入力し、予測を計算。予測が間違っていれば、その誤差と入力を用いて重みとバイアスを更新。これを繰り返すことで、モデルは徐々にデータに適合していく。

2.4 パーセプトロンの力と限界:線形分離可能性という条件

このシンプルな学習規則には、驚くべき理論的な保証があります。それがパーセプトロン収束定理です。

パーセプトロン収束定理 (Novikoff, 1962): もし、訓練データセットが**線形分離可能(Linearly Separable)**であり、学習率 η\eta が正の値であれば、パーセプトロン学習アルゴリズムは、有限回の更新(有限エポック)で、全ての訓練サンプルを正しく分類する重みベクトル w\mathbf{w} を見つけ出すことが保証される。

線形分離可能とは、特徴空間内に、全てのクラス1のサンプルと全てのクラス-1のサンプルを完全に分離する超平面(m=2m=2なら直線、m=3m=3なら平面)が存在することを意味します。

図2.3:線形分離可能性の例。左図は線形分離可能(赤い点線のような分離超平面が存在する)。右図は線形分離不可能(どんな直線/平面でも完全に分離できない)。

この定理は、パーセプトロンが(少なくとも線形分離可能な問題に対しては)確実に解を見つけられる強力なアルゴリズムであることを示しています。

しかし、この定理は同時に、パーセプトロンの根本的な限界も明らかにしています。もしデータセットが線形分離不可能ならば、収束は保証されません。学習プロセスは永遠に終わらず、重みは更新され続ける可能性があります(実際の実装では、最大エポック数で打ち切られます)。

XOR問題:線形分離不可能性の壁: この限界を示す最も有名な例がXOR(排他的論理和)問題です。 入力 (x1,x2)(x_1, x_2) に対する出力 yy は以下の通りです。 (0, 0) -> -1 (または 0) (0, 1) -> 1 (1, 0) -> 1 (1, 1) -> -1 (または 0) この4点を2次元平面上にプロットすると(図6.1参照)、クラス1の点 {(0,1),(1,0)}\{(0, 1), (1, 0)\} とクラス-1の点 {(0,0),(1,1)}\{(0, 0), (1, 1)\} を一本の直線で分離することは不可能です。したがって、単層のパーセプトロンではXOR問題を解くことができません。

この線形分離可能性という制約は、パーセプトロン、ひいては初期のコネクショニズム研究に対する大きな批判(特にMinskyとPapertによる3)を呼び、AI研究の方向性に影響を与えました。しかし、パーセプトロンが切り拓いた「データから学習する」という道筋と、そのシンプルな誤り訂正学習のアイデアは、決して色褪せることはありませんでした。それは、後のより強力なモデル開発のための重要な出発点となったのです。

次章では、この歴史的なアルゴリズムを、現代のツールであるPythonを使って自らの手で動かし、その挙動を体感してみましょう。


第3章:手を動かして理解する ― Pythonでパーセプトロンを実装

理論を学ぶことは重要ですが、アルゴリズムの真の挙動を理解するには、実際にコードを書いて動かしてみるのが一番です。本章では、第2章で学んだパーセプトロンをPythonで実装し、よく知られたIrisデータセットを使って、その学習プロセスと分類能力を体験します。この実践を通じて、理論とコード、そしてデータの間の繋がりを実感できるでしょう。

3.1 準備:プログラミング環境とライブラリ

この実装では、以下のPythonライブラリを使用します。これらはデータサイエンスや機械学習の分野で標準的に使われているものです。

これらのライブラリがインストールされていない場合は、Pythonのパッケージマネージャ(pipやconda)を使ってインストールしてください。 例: pip install numpy pandas matplotlib

3.2 パーセプトロンクラスの実装:理論をコードに翻訳する

第2章で説明したパーセプトロンの構造と学習規則を、Pythonのクラスとして実装します。クラスを使うことで、モデルのパラメータ(重みなど)と機能(学習、予測)をひとまとめにでき、コードが整理されて再利用しやすくなります。

# perceptron.py (この内容をファイルに保存するか、Jupyter Notebook等で実行)

import numpy as np

class Perceptron:
    """
    パーセプトロン分類器 (Perceptron classifier)

    シンプルな二値線形分類器。
    パーセプトロン学習規則に基づいて重みを更新します。

    Parameters
    ----------
    eta : float, default=0.01
        学習率 (Learning rate)。0.0より大きく1.0以下の値。
        各更新ステップのサイズを制御します。
    n_iter : int, default=50
        訓練データセットに対する反復回数(エポック数)。
        学習を何回繰り返すかの最大値。
    random_state : int, default=1
        重み初期化のための乱数生成器のシード。
        Noneにすると実行ごとに結果が変わる可能性があります。

    Attributes
    ----------
    w_ : 1次元配列 (1d-array)
        適合後の重み。配列の最初の要素(w_[0])がバイアス項、
        残りの要素(w_[1:])が入力特徴量に対する重みに対応します。
    errors_ : list
        各エポックでの誤分類(重み更新)の回数を格納したリスト。
        学習の進行状況を確認するのに役立ちます。

    Methods
    -------
    fit(X, y)
        訓練データにモデルを適合させます。
    net_input(X)
        総入力(線形結合)を計算します。
    predict(X)
        クラスラベル(+1または-1)を予測します。
    """
    def __init__(self, eta=0.01, n_iter=50, random_state=1):
        """コンストラクタ:パラメータを初期化"""
        self.eta = eta
        self.n_iter = n_iter
        self.random_state = random_state
        # self.w_ と self.errors_ は fit メソッド内で初期化・設定されます

    def fit(self, X, y):
        """
        訓練データを用いてパーセプトロンの重みを学習します。

        Parameters
        ----------
        X : 配列のような構造 (array-like), shape = [n_samples, n_features]
            訓練データ。n_samplesはサンプル数、n_featuresは特徴量の数。
        y : 配列のような構造 (array-like), shape = [n_samples]
            訓練データの正解ラベル。通常、+1と-1の値を取ります。

        Returns
        -------
        self : Perceptron
            学習済みの自身のインスタンスを返します。
        """
        # 乱数生成器を初期化 (再現性のため)
        rgen = np.random.RandomState(self.random_state)

        # 重みベクトル w_ を初期化します。
        # サイズは特徴量の数(X.shape[1]) + 1 (バイアス項w_[0]のため)。
        # 平均0, 標準偏差0.01の正規分布に従う小さな乱数で初期化。
        # これにより、初期の総入力が0付近になり、学習が安定しやすくなります。
        self.w_ = rgen.normal(loc=0.0, scale=0.01, size=1 + X.shape[1])

        # 各エポックでの誤分類数を記録するリストを初期化
        self.errors_ = []

        # 指定されたエポック数(n_iter)だけ学習ループを回します
        for _ in range(self.n_iter):
            # 現在のエポックでの誤分類(更新)回数をカウントする変数
            epoch_errors = 0

            # 訓練データセットの各サンプル (xi, target) について処理
            # zip(X, y) は X の各行と y の対応する要素をペアにして取り出します
            for xi, target in zip(X, y):
                # 1. 予測値を計算します (predictメソッドを呼び出し)
                #    predictメソッドは内部でnet_inputを計算し、ステップ関数を適用します
                prediction = self.predict(xi)

                # 2. 予測誤差を計算します (正解ラベル - 予測ラベル)
                #    予測が正しければ error は 0 になります。
                #    y=1, ŷ=-1 なら error=2; y=-1, ŷ=1 なら error=-2。
                error = target - prediction

                # 3. 重みを更新します
                #    更新量 update = 学習率 * 誤差
                update = self.eta * error
                #    特徴量に対する重み w_[1:] を更新: w_j := w_j + η * error * x_j
                #    NumPyのベクトル演算により、xiの各要素がupdate倍されてw_[1:]に加算されます。
                self.w_[1:] += update * xi
                #    バイアス項 w_[0] を更新: w_0 := w_0 + η * error * x_0 (x_0=1)
                self.w_[0] += update

                # 4. 誤分類(更新が発生したか)をカウントします
                #    updateが0でない <=> 予測が間違っていた ので、1を加算。
                epoch_errors += int(update != 0.0)

            # このエポックでの総誤分類数をリストに追加します
            self.errors_.append(epoch_errors)

            # (オプション) もしこのエポックで誤分類が0回だったら、
            # データは線形分離可能で、学習は完了した(収束した)とみなしてループを抜けることもできます。
            # if epoch_errors == 0:
            #     print(f"Converged in epoch {_+1}")
            #     break

        # 学習が完了した自身のインスタンスを返します
        return self

    def net_input(self, X):
        """
        総入力 z = w^T * x を計算します。

        Parameters
        ----------
        X : 配列のような構造 (array-like), shape = [n_samples, n_features] or [n_features]
            入力データ。単一サンプル(1D)または複数サンプル(2D)。

        Returns
        -------
        float or 1次元配列 (1d-array)
            計算された総入力。Xが単一サンプルの場合はスカラー値、複数サンプルの場合は各サンプルに対する総入力の配列。
        """
        # np.dot(X, self.w_[1:]) で w_1*x_1 + ... + w_m*x_m を計算。
        # Xが2D配列の場合、これは行列とベクトルの積になり、結果は1D配列になります。
        # その結果にバイアス項 self.w_[0] を加算します (NumPyのブロードキャスト機能)。
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def predict(self, X):
        """
        入力データ X に対するクラスラベル (+1 または -1) を予測します。

        Parameters
        ----------
        X : 配列のような構造 (array-like), shape = [n_samples, n_features] or [n_features]
            予測を行う入力データ。

        Returns
        -------
        int or 1次元配列 (1d-array)
            予測されたクラスラベル。Xが単一サンプルの場合は+1か-1、複数サンプルの場合はラベルの配列。
        """
        # net_inputメソッドで総入力 z を計算し、
        # np.where を使ってステップ関数を適用します。
        # z >= 0.0 ならば 1、そうでなければ -1 を返します。
        return np.where(self.net_input(X) >= 0.0, 1, -1)

コードのポイント解説:

3.3 実験データ:Iris(アヤメ)データセットの準備

理論を検証するために、実際のデータを使ってみましょう。機械学習の「Hello, World!」とも言えるIrisデータセットを用います。

# ライブラリのインポート
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# データセットの読み込み
# UCI Machine Learning Repositoryから直接読み込む試み
try:
    s = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
    print('Loading Iris dataset from URL:', s)
    df = pd.read_csv(s, header=None, encoding='utf-8')
except Exception as e:
    # URLからの読み込み失敗した場合、ローカルファイルを試す(事前にダウンロードが必要)
    # 例: カレントディレクトリに 'iris.data' がある場合
    local_path = 'iris.data'
    print(f"Could not load from URL ({e}). Trying local file: {local_path}")
    try:
        df = pd.read_csv(local_path, header=None, encoding='utf-8')
    except FileNotFoundError:
        print(f"Error: Local file {local_path} not found. Please download the dataset.")
        # データがない場合は処理を中断
        exit()

# データの内容確認(末尾5行を表示)
print("Dataset tail:")
print(df.tail())

# --- データの前処理 ---
# 1. SetosaとVersicolorのサンプルのみを抽出 (最初の100行)
#    同時に、クラスラベル(列4)を取得
y = df.iloc[0:100, 4].values
print("\nOriginal class labels (first 10):", y[:10])

# 2. クラスラベルを数値に変換 ('Iris-setosa' -> -1, 'Iris-versicolor' -> 1)
y = np.where(y == 'Iris-setosa', -1, 1)
print("Numerical class labels (first 10):", y[:10])
print("Numerical class labels (last 10):", y[-10:])

# 3. 特徴量を抽出 (がく片長: 列0, 花弁長: 列2)
X = df.iloc[0:100, [0, 2]].values
print("\nSelected features (first 5 samples):")
print(X[:5])

# --- データの可視化 ---
# Setosa (y=-1) と Versicolor (y=1) を散布図でプロット
plt.figure(figsize=(10, 7)) # グラフのサイズを少し大きめに

# Setosa (最初の50サンプル) をプロット
plt.scatter(X[:50, 0], X[:50, 1],
            color='red', marker='o', label='Setosa (Class -1)', s=50, alpha=0.8, edgecolors='w') # s: size, alpha: transparency, edgecolors: edge color

# Versicolor (次の50サンプル) をプロット
plt.scatter(X[50:100, 0], X[50:100, 1],
            color='blue', marker='x', label='Versicolor (Class 1)', s=50, alpha=0.8)

# 軸ラベルとタイトル、凡例の設定
plt.xlabel('Sepal length [cm]', fontsize=12)
plt.ylabel('Petal length [cm]', fontsize=12)
plt.title('Distribution of Iris Setosa and Versicolor', fontsize=14)
plt.legend(loc='upper left', fontsize=10)
plt.grid(True) # グリッド線を表示
plt.show()

図3.1:使用するIrisデータ(SetosaとVersicolor、がく片長と花弁長)。赤丸がSetosa(クラス-1)、青バツがVersicolor(クラス1)。視覚的に、これら2クラスは直線で分離できそうに見える。

この散布図(図3.1)を見ると、SetosaとVersicolorは、これら2つの特徴量だけでもかなり明確に分かれています。左下にSetosaのクラスター、右上にVersicolorのクラスターがあり、その間にはっきりと境界線が引けそうです。これは、パーセプトロンがうまく機能する(収束する)ための線形分離可能という条件を満たしている可能性が高いことを示唆しています。

3.4 パーセプトロンモデルの訓練と学習過程の評価

データが準備できたので、いよいよパーセプトロンモデルを訓練(学習)させます。先ほど実装したPerceptronクラスのインスタンスを作成し、fitメソッドに訓練データXと正解ラベルyを渡します。

# パーセプトロンのインスタンスを作成
# 学習率 eta = 0.1, エポック数 n_iter = 10 に設定
# random_state=1 で結果を固定
ppn = Perceptron(eta=0.1, n_iter=10, random_state=1)

# fitメソッドを呼び出してモデルを訓練
print("\nTraining Perceptron...")
ppn.fit(X, y)
print("Training finished.")

# 学習後の重みを確認 (w_[0]がバイアス)
print(f"Learned weights: w_ = {ppn.w_}")

# 学習過程(各エポックでの誤分類数)をプロットして評価
plt.figure(figsize=(10, 7))
plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, marker='o', linestyle='-', color='b')
plt.xlabel('Epochs', fontsize=12)
plt.ylabel('Number of updates (Misclassifications)', fontsize=12)
plt.title('Perceptron Learning Curve', fontsize=14)
plt.xticks(range(1, len(ppn.errors_) + 1)) # エポック数を整数で表示
plt.grid(True)
plt.show()

図3.2:パーセプトロンの学習曲線(エポックごとの誤分類数)。エポックが進むにつれて誤分類数が急速に減少し、6エポック目で0となり、安定している。これは学習が成功し、モデルが収束したことを示している。

結果の分析: 学習曲線(図3.2)を見ると、最初のエポックでは多くの誤分類(重み更新)が発生していますが、エポックが進むにつれてその数は劇的に減少しています。そして、6回目のエポック以降は誤分類数が0になっています。これは、パーセプトロンが訓練データセット全体を完全に分類できる決定境界(重みベクトル)を見つけ、学習が収束したことを意味します。これは、パーセプトロン収束定理が保証する通りの結果であり、データが線形分離可能であったことを裏付けています。

3.5 決定境界の可視化:学習結果を視覚的に理解する

学習が成功したことは分かりましたが、モデルが具体的にどのような分類ルール(決定境界)を獲得したのかを視覚的に確認しましょう。決定境界は、モデルの予測がクラス-1からクラス1に切り替わる境界線(この場合は直線)です。

以下のヘルパー関数 plot_decision_regions は、特徴量空間を細かいグリッドに分割し、各グリッド点でのモデルの予測クラスに応じて背景色を塗り分けます。その上に実際の訓練データを重ねてプロットすることで、決定境界とデータの関係が一目でわかるようになります。

# plot_decision_regions.py (またはNotebookセル)

from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
import numpy as np

def plot_decision_regions(X, y, classifier, resolution=0.02, xlabel='Feature 1', ylabel='Feature 2', title='Decision Regions'):
    """
    2次元データセットに対する分類器の決定領域をプロットします。

    Parameters
    ----------
    X : array-like, shape = [n_samples, 2]
        特徴量データ。2次元である必要があります。
    y : array-like, shape = [n_samples]
        正解ラベル。
    classifier : object
        訓練済みの分類器オブジェクト。predictメソッドを持つ必要があります。
    resolution : float, default=0.02
        グリッドの解像度。小さいほど滑らかですが計算量が増えます。
    xlabel, ylabel, title : str
        グラフのラベルとタイトル。
    """
    # マーカーと色の設定
    markers = ('s', 'x', 'o', '^', 'v') # クラスごとのマーカー形状
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan') # クラスごとの色
    # 使用する色の数だけカラーマップを作成
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # 特徴量の最小値・最大値を見つけ、プロット範囲を少し広げる
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1

    # 指定された解像度でグリッドポイント(格子状の点)を生成
    # np.meshgridは、x1軸とx2軸の座標ベクトルから、格子点の座標行列を作成します
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))

    # 各グリッドポイントでの予測クラスを計算
    # xx1.ravel() と xx2.ravel() は、グリッド座標を行列から1次元配列に変換します
    # np.array([...]).T は、[特徴量1の全点, 特徴量2の全点] という形の2次元配列を作成します
    # これを分類器のpredictメソッドに入力します
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)

    # 予測結果Zも1次元配列なので、元のグリッド形状 (xx1と同じ形状) に戻します
    Z = Z.reshape(xx1.shape)

    # グリッドに対して予測結果に基づいて色を塗り、決定領域を描画します
    # plt.contourf は等高線を描画し、その間を塗りつぶします
    plt.figure(figsize=(10, 7)) # 描画サイズを指定
    plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap) # alphaで透明度を指定

    # プロット範囲を設定
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())

    # 訓練サンプルを重ねてプロット
    # np.unique(y) で存在するクラスラベルを取得し、クラスごとにループ
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0],  # yがclであるサンプルの特徴量1 (x座標)
                    y=X[y == cl, 1],  # yがclであるサンプルの特徴量2 (y座標)
                    alpha=0.8,
                    c=colors[idx],         # クラスに応じた色
                    marker=markers[idx],   # クラスに応じたマーカー
                    label=f'Class {cl}',   # 凡例のラベル
                    edgecolor='black',     # マーカーの縁取り
                    s=50)                  # マーカーサイズ

    # ラベル、タイトル、凡例、グリッドを設定
    plt.xlabel(xlabel, fontsize=12)
    plt.ylabel(ylabel, fontsize=12)
    plt.title(title, fontsize=14)
    plt.legend(loc='upper left', fontsize=10)
    plt.grid(True)
    plt.show()

# --- 決定境界のプロットを実行 ---
plot_decision_regions(X, y, classifier=ppn,
                      xlabel='Sepal length [cm]',
                      ylabel='Petal length [cm]',
                      title='Perceptron Decision Boundary on Iris Data')

図3.3:学習後のパーセプトロンによって得られた決定境界。背景の赤色領域がクラス-1 (Setosa) と予測される領域、青色領域がクラス1 (Versicolor) と予測される領域を示す。境界線(色の変わり目)が線形(直線)であり、全ての訓練サンプルを正しく分離していることがわかる。

結果の解釈: 図3.3は、訓練されたパーセプトロンが学習した決定境界を明確に示しています。赤色の領域と青色の領域を分ける直線が、モデルが獲得した分類ルールです。この直線よりも左下に位置する点はSetosa、右上に位置する点はVersicolorと予測されます。重要なのは、全ての赤丸(Setosa)が赤色領域に、全ての青バツ(Versicolor)が青色領域に正しく含まれていることです。これは、パーセプトロンがこの線形分離可能な問題に対して、完全に機能する分類器を学習できたことを視覚的に裏付けています。

3.6 実装からの学びと次のステップ

この章の実装と実験を通じて、以下の点が明らかになりました。

しかし、同時にパーセプトロンの限界も念頭に置く必要があります。

これらの課題に対処し、より堅牢で汎用的な学習アルゴリズムを求めて、研究者たちは新たなモデルを開発しました。その中でも、パーセプトロンと密接に関連しつつ、勾配降下法という強力な最適化手法を導入したADALINEは、特に重要な位置を占めます。次章では、ADALINEの理論へと進みましょう。


第4章:最適化への道筋 ― ADALINEと勾配降下法

パーセプトロンは「学習する機械」の概念を打ち立てましたが、その学習プロセス(誤分類時にのみ更新)や線形分離可能性という制約には改善の余地がありました。パーセプトロンとほぼ同時期、1960年にスタンフォード大学の**バーナード・ウィドロウ(Bernard Widrow)と彼の学生テッド・ホフ(Ted Hoff)は、ADALINE(Adaptive Linear Neuron)という、より洗練された学習メカニズムを持つモデルを提案しました4。ADALINEは、現代の多くの機械学習アルゴリズムの根幹をなす目的関数(コスト関数)の最小化勾配降下法(Gradient Descent)**という考え方を導入した点で、非常に重要です。

4.1 ADALINEの構造:パーセプトロンとの決定的な違い

一見すると、ADALINEの基本構造はパーセプトロンと酷似しています。入力ベクトル x\mathbf{x} を受け取り、重みベクトル w\mathbf{w} を用いて総入力 z=wTxz = \mathbf{w}^T \mathbf{x} (バイアス項 w0w_0 を含む)を計算する点までは同じです。

決定的な違いは、重み w\mathbf{w} を更新するために何を使うか、という点にあります。

なぜこの違いが重要なのか? ADALINEは、パーセプトロンのように「間違っているか、合っているか」だけでなく、「どれくらい間違っているか」という誤差の大きさの情報を学習に利用します。総入力 zz は連続値なので、誤差 (yz)(y-z) も連続値となり、より滑らかで数学的に扱いやすい学習プロセスが可能になります。

図4.1:ADALINE(右)とパーセプトロン(左)の重み更新における情報フローの違い。ADALINEは線形ユニットの出力 zz と真のラベル yy を比較して誤差を計算し、それを重み更新に使う。パーセプトロンはステップ関数適用後の出力 y^\hat{y} を使う。

4.2 学習の「目標」を定める:目的関数(コスト関数)

ADALINEの学習アプローチの核心は、まず目的関数(Objective Function)またはコスト関数(Cost Function) J(w)J(\mathbf{w}) を定義することにあります。これは、現在の重みベクトル w\mathbf{w} が、与えられた訓練データに対してどれだけ「性能が悪いか」を測るための「ものさし」となる関数です。学習の目標は、このコスト関数 J(w)J(\mathbf{w}) の値を最小にするような重み w\mathbf{w} を見つけることになります。

ADALINEで標準的に用いられるコスト関数は、二乗誤差和(Sum of Squared Errors, SSE)です。これは、全ての訓練サンプル ii (i=1,,ni=1, \dots, n) について、真のラベル y(i)y^{(i)} と、現在の重み w\mathbf{w} による線形ユニットの出力 z(i)=wTx(i)z^{(i)} = \mathbf{w}^T \mathbf{x}^{(i)} との差(誤差)の二乗を計算し、それらを合計したものです。

J(w)=12i=1n(y(i)z(i))2サンプルiの二乗誤差=12i=1n(y(i)wTx(i))2(4.1)J(\mathbf{w}) = \frac{1}{2} \sum_{i=1}^{n} \underbrace{\left( y^{(i)} - z^{(i)} \right)^2}_{\text{サンプルiの二乗誤差}} = \frac{1}{2} \sum_{i=1}^{n} \left( y^{(i)} - \mathbf{w}^T \mathbf{x}^{(i)} \right)^2 \tag{4.1}

なぜ二乗誤差なのか?

係数 1/21/2 の意味: これは微分した際に指数「2」と打ち消し合って式を綺麗にするための慣習的なものであり、コスト関数の最小値を与える w\mathbf{w} の位置には影響しません。

このコスト関数 J(w)J(\mathbf{w}) は、重み w\mathbf{w} ( w0,w1,,wmw_0, w_1, \dots, w_mm+1m+1 個の変数) の関数とみなせます。その形状は、m+1m+1 次元の空間における「ボウル」のような形(専門的には凸関数 (convex function))をしています。学習の目標は、このボウルの最も低い底に対応する重み w\mathbf{w}^* を見つけることです。

4.3 最適化手法:勾配降下法で谷底へ

コスト関数 J(w)J(\mathbf{w}) を最小化する w\mathbf{w} を見つけるための、強力で広く使われるアルゴリズムが**勾配降下法(Gradient Descent)**です。その名の通り、「勾配」、つまり関数が各点で最も急に増加する方向、を利用して、その逆方向(最も急な下り坂の方向)へと少しずつ進んでいくことで、最小値(谷底)を探す反復的な手法です。

アイデア: 想像してみてください。あなたは深い霧の中で山の斜面に立っており、谷底(標高が最も低い場所)を目指したいとします。視界が悪くても、足元の地面の傾き(勾配)は感じられます。最も急な下り坂の方向へ一歩進み、またそこでの傾きを調べて最も急な下り方向へ一歩進む…これを繰り返せば、いずれ谷底にたどり着けるでしょう。勾配降下法は、これと同じことを数学的に行います。

ステップ:

  1. 初期化: 重みベクトル w\mathbf{w} を適当な値(例:ゼロベクトルや小さな乱数値)で初期化します。

  2. 反復: 以下の更新を、コスト J(w)J(\mathbf{w}) が十分に小さくなるか、変化しなくなるまで繰り返します。 a. 勾配計算: 現在の重み w\mathbf{w} におけるコスト関数 J(w)J(\mathbf{w})勾配 J(w)\nabla J(\mathbf{w}) を計算します。勾配は、各重み wjw_j に関する偏微分のベクトルです。

    J(w)=[Jw0Jw1Jwm]\nabla J(\mathbf{w}) = \begin{bmatrix} \frac{\partial J}{\partial w_0} \\ \frac{\partial J}{\partial w_1} \\ \vdots \\ \frac{\partial J}{\partial w_m} \end{bmatrix}

    この勾配ベクトルは、現在の点 w\mathbf{w} から少し動いたときに、J(w)J(\mathbf{w}) が最も増加する方向を指します。 b. 重み更新: 現在の重み w\mathbf{w} を、負の勾配方向J(w)-\nabla J(\mathbf{w})、つまり最も急な下り方向)へ、学習率 η\eta で定められた歩幅だけ進めます。

    w:=wηJ(w)(4.4a)\mathbf{w} := \mathbf{w} - \eta \nabla J(\mathbf{w}) \tag{4.4a}

    または、更新量 Δw=ηJ(w)\Delta \mathbf{w} = - \eta \nabla J(\mathbf{w}) として、

    w:=w+Δw(4.4b)\mathbf{w} := \mathbf{w} + \Delta \mathbf{w} \tag{4.4b}

ADALINEの勾配計算: 式(4.1)のコスト関数 J(w)J(\mathbf{w}) を各重み wjw_j (j=0,,mj=0, \dots, m) で偏微分すると、勾配の各成分が得られます。(添え字 ii はサンプルインデックス、添え字 j,kj, k は重み/特徴量インデックスです。)

Jwj=wj[12i=1n(y(i)k=0mwkxk(i))2]\frac{\partial J}{\partial w_j} = \frac{\partial}{\partial w_j} \left[ \frac{1}{2} \sum_{i=1}^{n} (y^{(i)} - \sum_{k=0}^{m} w_k x_k^{(i)})^2 \right]

連鎖律 ddxf(g(x))=f(g(x))g(x)\frac{d}{dx} f(g(x)) = f'(g(x)) g'(x)ddxx2=2x\frac{d}{dx} x^2 = 2x を使うと、

=12i=1n2(y(i)k=0mwkxk(i))wj(y(i)k=0mwkxk(i))= \frac{1}{2} \sum_{i=1}^{n} 2 (y^{(i)} - \sum_{k=0}^{m} w_k x_k^{(i)}) \cdot \frac{\partial}{\partial w_j} (y^{(i)} - \sum_{k=0}^{m} w_k x_k^{(i)})

内側の微分は、k=0mwkxk(i)\sum_{k=0}^{m} w_k x_k^{(i)}wjw_j で偏微分すると xj(i)x_j^{(i)} だけが残るので(y(i)y^{(i)}wjw_j に依存しない定数)、xj(i)-x_j^{(i)} となります。(ここで x0(i)=1x_0^{(i)} = 1 を思い出してください)

=i=1n(y(i)k=0mwkxk(i))(xj(i))=i=1n(y(i)z(i))誤差xj(i)(4.2 再掲)= \sum_{i=1}^{n} (y^{(i)} - \sum_{k=0}^{m} w_k x_k^{(i)}) (-x_j^{(i)}) = - \sum_{i=1}^{n} \underbrace{(y^{(i)} - z^{(i)})}_{\text{誤差}} x_j^{(i)} \tag{4.2 再掲}

したがって、勾配ベクトル全体は、

J(w)=i=1n(y(i)z(i))x(i)(4.3 再掲)\nabla J(\mathbf{w}) = - \sum_{i=1}^{n} (y^{(i)} - z^{(i)}) \mathbf{x}^{(i)} \tag{4.3 再掲}

となります。

ADALINEの重み更新式(バッチ勾配降下法): これを式(4.4a)に代入すると、ADALINEの重み更新式が得られます。

w:=wη(i=1n(y(i)z(i))x(i))\mathbf{w} := \mathbf{w} - \eta \left( - \sum_{i=1}^{n} (y^{(i)} - z^{(i)}) \mathbf{x}^{(i)} \right) w:=w+ηi=1n(y(i)z(i))x(i)(4.5 再掲)\mathbf{w} := \mathbf{w} + \eta \sum_{i=1}^{n} (y^{(i)} - z^{(i)}) \mathbf{x}^{(i)} \tag{4.5 再掲}

各成分 wjw_j について書くと、

wj:=wj+ηi=1n(y(i)z(i))xj(i)(for j=0,,m)w_j := w_j + \eta \sum_{i=1}^{n} (y^{(i)} - z^{(i)}) x_j^{(i)} \quad (\text{for } j=0, \dots, m)

となります。

図4.2:2次元の重み空間 (w1,w2)(w_1, w_2) におけるコスト関数 J(w1,w2)J(w_1, w_2) の等高線と勾配降下のステップ。各ステップで勾配(矢印)の逆方向に移動し、最小値(中心)に近づいていく。学習率 η\eta がステップの大きさを決める。

4.4 勾配降下法のバリエーション:効率と安定性のトレードオフ

式(4.5)で導出した更新方法は、1回の重み更新のために訓練データ全体の誤差を合計して勾配を計算するため、**バッチ勾配降下法(Batch Gradient Descent, BGD)**と呼ばれます。

この計算コストの問題を解決するため、実用上は以下のバリエーションがよく使われます。

アルゴリズム更新に使うデータ数更新頻度 (1エポックあたり)計算コスト (1更新)メモリ使用量収束の安定性オンライン適性
BGDnn (全データ)1回
SGD1nn低 (ノイズ大)
Mini-batch GDkk (1<k<n1 < k < n)n/kn/k

表4.1:勾配降下法の主なバリエーションとその特徴比較。

ADALINEは、この強力な勾配降下法という最適化の枠組みを機械学習に導入したことで、パーセプトロンが抱えていたいくつかの課題(特に学習プロセスの数学的基盤)を克服しました。また、コスト関数 J(w)J(\mathbf{w}) を定義したことで、学習の進行状況を客観的に監視し、理論的な解析を行う道も開かれました。

次章では、このADALINEをSGDを用いてPythonで実装し、パーセプトロンとの挙動の違いや、勾配降下法特有の注意点、特に特徴量スケーリングの重要性について、実践を通じて深く掘り下げていきます。


第5章:ADALINEの実装と実践 ― 特徴量スケーリングの重要性

理論を学んだところで、ADALINEアルゴリズムを実際にプログラミングし、その挙動を確認しましょう。本章では、前章で解説した**確率的勾配降下法(SGD)を用いるADALINEクラスをPythonで実装します。そして、この実装を通じて、勾配降下法に基づく多くのアルゴリズムで成功の鍵となる特徴量スケーリング(Feature Scaling)**の不可欠性を体験的に学びます。

5.1 AdalineSGDクラス:SGDによる学習の実装

Perceptronクラスと同様に、AdalineSGDクラスを定義します。主な違いは、fitメソッド内での重み更新ロジックと、コスト(損失)の計算・記録方法にあります。また、SGDのパフォーマンスを改善するために一般的に行われる、エポックごとのデータシャッフル機能も組み込みます。

# adalinesgd.py (またはNotebookセル)

import numpy as np
from numpy.random import seed
import matplotlib.pyplot as plt # 可視化用にインポートしておく

class AdalineSGD:
    """
    確率的勾配降下法 (SGD) を用いたADAptive LInear NEuron分類器。

    Parameters
    ----------
    eta : float, default=0.01
        学習率 (0.0 < eta <= 1.0)。
    n_iter : int, default=10
        訓練データセットに対する反復回数(エポック数)。
    shuffle : bool, default=True
        Trueの場合、各エポックで訓練データをシャッフルし、
        サンプルの順序への依存や循環を防ぎます。
    random_state : int, default=None
        重み初期化とシャッフルのための乱数生成器のシード。
        再現性が必要な場合に設定します。

    Attributes
    ----------
    w_ : 1次元配列
        適合後の重み(バイアス項含む)。
    losses_ : list
        各エポックでの平均二乗誤差コスト(損失)の履歴。
    w_initialized : bool
        重みが初期化されたかどうかのフラグ(partial_fit用)。

    Methods
    -------
    fit(X, y)
        訓練データにモデルを適合させます。
    partial_fit(X, y)
        重みを再初期化せずにオンラインで学習します。
    net_input(X)
        総入力(線形結合)を計算します。
    predict(X)
        クラスラベル(+1または-1)を予測します。
    _initialize_weights(m)
        重みを初期化します (内部メソッド)。
    _update_weights(xi, target)
        単一サンプルで重みを更新し、コストを返します (内部メソッド)。
    _shuffle(X, y)
        データをシャッフルします (内部メソッド)。
    """
    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.w_initialized = False
        self.shuffle = shuffle
        self.random_state = random_state
        # random_stateが指定されていれば、NumPyの乱数シードを設定
        if random_state:
            seed(random_state)

    def fit(self, X, y):
        """
        訓練データ全体を用いてモデルを学習します。

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
        y : array-like, shape = [n_samples], Labels: {+1, -1}

        Returns
        -------
        self : object
        """
        # 1. 重みの初期化 (初回のみ)
        self._initialize_weights(X.shape[1])
        # 2. コスト履歴を格納するリストを初期化
        self.losses_ = []

        # 3. エポック数だけ学習を繰り返す
        for i in range(self.n_iter):
            # 4. (オプション) データをシャッフル
            if self.shuffle:
                X, y = self._shuffle(X, y)

            # 5. このエポックでの各サンプルのコストを格納するリスト
            costs = []
            # 6. 各訓練サンプル (xi, target) で SGD 更新
            for xi, target in zip(X, y):
                # 6a. 重みを更新し、そのサンプルのコストを取得
                cost = self._update_weights(xi, target)
                costs.append(cost)

            # 7. エポック全体の平均コストを計算し、履歴に追加
            avg_cost = sum(costs) / len(y)
            self.losses_.append(avg_cost)

        return self

    def partial_fit(self, X, y):
        """
        既存の重みを使って、新しいデータで追加学習(オンライン学習)。
        重みは再初期化されません。

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
        y : array-like, shape = [n_samples]

        Returns
        -------
        self : object
        """
        # もし重みがまだ初期化されていなければ、初期化する
        if not self.w_initialized:
            self._initialize_weights(X.shape[1])

        # 新しいデータサンプル (xi, target) で重みを更新
        # y.ravel().shape[0] > 0 は、y に1つ以上の要素があるかを確認
        if y.ravel().shape[0] > 0:
            for xi, target in zip(X, y):
                self._update_weights(xi, target)
        # (書籍に合わせた実装:もしyがスカラー(1サンプル)なら)
        # else:
        #     self._update_weights(X, y) # Xも単一サンプルと仮定

        return self

    def _shuffle(self, X, y):
        """訓練データをランダムにシャッフルします"""
        # np.random.permutation は 0 から len(y)-1 までのインデックスの
        # ランダムな並び替え(順列)を生成します。
        r = np.random.permutation(len(y))
        # この順列を使って X と y の行(サンプル)を並べ替えます。
        return X[r], y[r]

    def _initialize_weights(self, m):
        """重みベクトルをゼロに近い乱数で初期化します"""
        # サイズ m+1 のベクトルを生成 (m: 特徴量数, +1: バイアス項)
        # loc=0.0 (平均), scale=0.01 (標準偏差) の正規分布に従う乱数。
        self.w_ = np.random.normal(loc=0.0, scale=0.01, size=1 + m)
        self.w_initialized = True # 初期化済みフラグを立てる

    def _update_weights(self, xi, target):
        """
        ADALINEの学習規則 (SGD) に基づいて重みを更新し、
        二乗誤差コストを返します。

        Parameters
        ----------
        xi : 1次元配列 (1d-array)
            単一の訓練サンプルの特徴量ベクトル。
        target : int (+1 or -1)
            単一の訓練サンプルの正解ラベル。

        Returns
        -------
        float
            このサンプルに対する二乗誤差コスト (0.5 * error^2)。
        """
        # 1. 線形活性化関数(総入力)の出力を計算: output = z = w^T * x
        output = self.net_input(xi)

        # 2. 誤差を計算: error = y - z
        error = (target - output)

        # 3. 重みを更新: w := w + η * error * x
        #    (w_[1:] は特徴量の重み, w_[0] はバイアス)
        self.w_[1:] += self.eta * xi * error
        self.w_[0] += self.eta * error

        # 4. 二乗誤差コストを計算: cost = 0.5 * error^2
        cost = 0.5 * error**2
        return cost

    def net_input(self, X):
        """総入力 z = w^T * x を計算"""
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def predict(self, X):
        """最終的なクラスラベル (+1 or -1) を予測"""
        # net_input の結果をステップ関数(閾値0)に通す
        return np.where(self.net_input(X) >= 0.0, 1, -1)

実装のポイント:

5.2 勾配降下法の落とし穴:特徴量スケーリングの必要性

さあ、実装したAdalineSGDをIrisデータで試してみましょう。…とその前に、勾配降下法を用いる際に避けては通れない、非常に重要な前処理について説明します。それが**特徴量スケーリング(Feature Scaling)**です。

なぜスケーリングが不可欠なのか?

勾配降下法の更新式 w:=w+η(error)x\mathbf{w} := \mathbf{w} + \eta (\text{error}) \mathbf{x} を思い出してください。重みの更新量は、入力特徴量 x\mathbf{x} の値に直接比例します。もし、特徴量によって値のスケール(範囲や大きさ)が大きく異なっていると、どうなるでしょうか?

例えば、ある特徴量 x1x_1 が家の面積(例:50~200 [m²])で、x2x_2 が部屋数(例:1~5 [部屋])だったとします。x1x_1 の値は x2x_2 の値より数十倍大きいため、x1x_1 に対応する重み w1w_1 の勾配および更新量も、w2w_2 のそれより(誤差が同じでも)数十倍大きくなる傾向があります。

これがコスト関数 J(w)J(\mathbf{w}) の形状に与える影響は深刻です。コスト関数の等高線は、スケールの大きい特徴量の軸方向に引き伸ばされ、非常に細長い谷のような形になります(図5.1参照)。このような「地形」の上で勾配降下法を実行すると、以下のような問題が生じます。

図5.1:左: 特徴量のスケールが異なると、コスト関数の等高線は歪んだ楕円形になり、勾配降下は非効率な経路(赤線)を辿る。適切な学習率の設定も難しい。右: 特徴量をスケーリングすると、等高線は円に近くなり、勾配はほぼ最小値の方向を向き、効率的に収束する(青線)。

解決策:スケールを揃える

幸い、この問題には効果的な解決策があります。学習アルゴリズムにデータを入力する前に、全ての特徴量が同程度のスケールを持つように変換する前処理、すなわち特徴量スケーリングを行えばよいのです。代表的な手法には以下があります。

  1. 標準化(Standardization) (Z-score normalization): 各特徴量 xjx_j からその平均値 μj\mu_j を引き、標準偏差 σj\sigma_j で割ることで、平均が 0、標準偏差が 1 の分布に変換します。

    xj=xjμjσj(5.1)x'_{j} = \frac{x_j - \mu_j}{\sigma_j} \tag{5.1}

    特徴: 元の分布の形状を比較的保ちます。外れ値の影響は受けますが、正規化ほどではありません。多くのアルゴリズム(特に距離や勾配に基づくもの)で推奨されます。

  2. 正規化(Normalization) (Min-Max Scaling): 各特徴量 xjx_j を、最小値 min(xj)\min(x_j) と最大値 max(xj)\max(x_j) を使って、特定の範囲、例えば [0,1][0, 1] に線形に変換します。

    xj=xjmin(xj)max(xj)min(xj)(5.2)x'_{j} = \frac{x_j - \min(x_j)}{\max(x_j) - \min(x_j)} \tag{5.2}

    特徴: 値の範囲が明確に定まります(例: 0から1)。ただし、外れ値が1つでもあると、他のほとんどの値が非常に狭い範囲に押し込められてしまう可能性があります。

どちらを使うかは状況によりますが、一般的には標準化がより広く使われています。今回は標準化を採用しましょう。

# Irisデータ X (sepal length, petal length) を標準化

# まず、元のデータ X をコピーして、元のデータを変更しないようにします
X_std = np.copy(X)

# 特徴量ごと(列ごと)に標準化を行います
# 列0 (Sepal length) の標準化
mean_sepal_length = X[:, 0].mean()
std_sepal_length = X[:, 0].std()
X_std[:, 0] = (X[:, 0] - mean_sepal_length) / std_sepal_length

# 列1 (Petal length) の標準化
mean_petal_length = X[:, 1].mean()
std_petal_length = X[:, 1].std()
X_std[:, 1] = (X[:, 1] - mean_petal_length) / std_petal_length

# (参考) Scikit-learn を使えばもっと簡単にできます
# from sklearn.preprocessing import StandardScaler
# sc = StandardScaler()
# X_std_sklearn = sc.fit_transform(X) # fitで平均・標準偏差を計算し、transformで適用
# assert np.allclose(X_std, X_std_sklearn) # 結果がほぼ同じことを確認

これで、X_std には、平均0、標準偏差1にスケーリングされた特徴量データが格納されました。

5.3 標準化データを用いたADALINE(SGD)の訓練と結果評価

準備が整ったので、標準化されたデータ X_std とラベル y を用いて、AdalineSGD モデルを訓練します。学習率 η=0.01\eta=0.01、エポック数 n_iter=15n\_iter=15 で試してみましょう。

# AdalineSGDのインスタンスを作成
# 学習率 0.01, エポック数 15, シャッフル有効, 再現性のため乱数シード1
ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)

# 標準化されたデータ X_std とラベル y でモデルを訓練
print("\nTraining AdalineSGD with standardized data...")
ada_sgd.fit(X_std, y)
print("Training finished.")

# 学習後の重みを確認
print(f"Learned weights (standardized): w_ = {ada_sgd.w_}")

# --- 結果の可視化 ---
# 1. 決定境界のプロット
#    (plot_decision_regions 関数は第3章で定義済みとします)
plot_decision_regions(X_std, y, classifier=ada_sgd,
                      xlabel='Sepal length [standardized]',
                      ylabel='Petal length [standardized]',
                      title='Adaline (SGD) Decision Boundary (Standardized Data)')

# 2. 学習曲線(コスト関数の推移)のプロット
plt.figure(figsize=(10, 7))
plt.plot(range(1, len(ada_sgd.losses_) + 1), ada_sgd.losses_, marker='o', linestyle='-', color='g')
plt.xlabel('Epochs', fontsize=12)
plt.ylabel('Average Cost (SSE)', fontsize=12)
plt.title('Adaline (SGD) Learning Curve (Standardized Data, eta=0.01)', fontsize=14)
plt.grid(True)
plt.show()

図5.2:標準化データで学習したADALINE(SGD)の決定境界。データ点をきれいに分離する直線が得られている。軸のラベルが「standardized」となっている点に注意。

図5.3:標準化データを用いたADALINE(SGD)の学習曲線(平均コストの推移)。エポックが進むにつれてコストが滑らかに減少し、約10エポックでほぼ最小値に収束していることがわかる。学習は安定している。

結果の分析:

5.4 スケーリングの効果:もしスケーリングしなかったら?

比較のために、もし標準化を行わずに元のデータ X を使って、同じ学習率 η=0.01\eta=0.01AdalineSGD を訓練したらどうなるでしょうか?

# (比較実験) スケーリングなしのデータで訓練
ada_sgd_unscaled = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
print("\nTraining AdalineSGD with unscaled data (eta=0.01)...")
ada_sgd_unscaled.fit(X, y) # X を使用
print("Training finished.")

# 学習曲線をプロット
plt.figure(figsize=(10, 7))
plt.plot(range(1, len(ada_sgd_unscaled.losses_) + 1), ada_sgd_unscaled.losses_, marker='o', linestyle='-', color='r')
plt.xlabel('Epochs', fontsize=12)
plt.ylabel('Average Cost (SSE)', fontsize=12)
plt.title('Adaline (SGD) Learning Curve (Unscaled Data, eta=0.01)', fontsize=14)
plt.ylim(0, max(ada_sgd_unscaled.losses_[1:]) * 1.1) # y軸の範囲を調整 (初回を除く)
plt.grid(True)
plt.show()

図5.4:スケーリングされていない元のデータを用いた場合のADALINE(SGD)の学習曲線(η=0.01\eta=0.01)。コストが減るどころか、エポックが進むにつれて指数関数的に増大し、発散してしまっている。

結果の分析(図5.4): 衝撃的な結果です。学習率 η=0.01\eta=0.01 では、コスト関数が全く減少しません。それどころか、エポックが進むにつれて急激に増大し、完全に発散してしまっています。これは、元の特徴量のスケール(特にSepal lengthの値が大きい)に対して学習率が大きすぎたため、更新ステップが谷底を飛び越えてしまい、コストが増加する方向へ進んでしまったことを示唆しています。

この問題に対処するには、学習率を劇的に小さくする(例:η=0.0001\eta=0.0001)必要がありますが、それでも収束にはるかに多くのエポックが必要になるでしょう。

結論: この比較実験は、勾配降下法を用いるアルゴリズム(ADALINE、そして後のロジスティック回帰、SVM、ニューラルネットワークなど)にとって、特徴量スケーリングがいかに重要であるかを明確に示しています。スケーリングは、単なる「推奨されるテクニック」ではなく、多くの場合、学習を成功させるための必須の前処理ステップなのです。

これで、ADALINEとその学習における重要な実践的側面についての理解が深まりました。しかし、ADALINEもまた線形分類器であり、線形分離不可能な問題は解けません。次章では、この線形性の壁をどのように乗り越え、現代の強力な非線形モデルへと道が開かれたのかを見ていきます。


第6章:線形モデルの壁と未来 ― MLPと深層学習へ

パーセプトロンとADALINEは、機械学習の基礎を築いた重要なアルゴリズムですが、どちらも線形分類器であるという本質的な限界を持っています。つまり、データを分離するために直線(または高次元では超平面)しか引くことができません。しかし、現実世界の問題の多くは、もっと複雑で、非線形な関係性を持っています。本章では、線形モデルが直面した壁、それを乗り越えるためのアイデア、そしてそれが現代の多層パーセプトロン(MLP)深層学習へとどのようにつながっていったのかを探ります。

6.1 線形性の限界、再び:XOR問題という象徴

線形モデルが解けない最も単純かつ有名な問題が、繰り返し登場するXOR(排他的論理和)問題です。入力 (x1,x2)(x_1, x_2) に対して、 (0,0)0(0,0)\to 0, (0,1)1(0,1)\to 1, (1,0)1(1,0)\to 1, (1,1)0(1,1)\to 0 という出力を要求します(クラスラベルを-1と1にしても同じです)。

図6.1:XOR問題の4つのデータ点。クラス0(赤丸)とクラス1(青バツ)を分類したいが、これらを1本の直線で完全に分離することは不可能である(線形分離不可能)。

図6.1が示すように、この4点を分類するためには、非直線的な境界線が必要です。単層のパーセプトロンやADALINEは、その構造上、線形の決定境界しか作れないため、この単純な論理問題すら解くことができません。

この事実は、初期のAI研究(特にコネクショニズム)にとって大きな打撃となりました。もし基本的な論理演算すらモデル化できないのであれば、人間の知能のような複雑な機能を模倣することなど到底できないのではないか、という悲観論(いわゆる第一次AIブームの後の「AIの冬」)を生む一因ともなりました。線形モデルの限界を克服することが、次の大きな課題となったのです。

6.2 壁を越えるためのアイデア:非線形性へのアプローチ

研究者たちは、線形モデルの限界を超えるために、様々な方向性を模索しました。

  1. 特徴量エンジニアリング(Feature Engineering): これは、元の入力特徴量 x1,x2,x_1, x_2, \dots から、それらの非線形な組み合わせによって新しい特徴量を作り出し、それを線形モデルに入力するというアプローチです。例えば、XOR問題では、x1x_1x2x_2 に加えて、それらの積 x3=x1x2x_3 = x_1 x_2 を新しい特徴量として考えます。すると、4つの点は3次元空間 (x1,x2,x3)(x_1, x_2, x_3)(0,0,0)(0,0,0), (0,1,0)(0,1,0), (1,0,0)(1,0,0), (1,1,1)(1,1,1) となり、これは平面 x30.5=0x_3 - 0.5 = 0 (例えば)のような線形な境界で分離可能になります。 利点: 線形モデルのアルゴリズムをそのまま使える。 欠点: どのような特徴量変換が有効かは問題ごとに異なり、人間の洞察力や試行錯誤が必要(自動化が難しい)。特徴量数が爆発的に増加する可能性もある。

  2. カーネル法(Kernel Methods): データを明示的に高次元空間に写像する代わりに、カーネル関数 K(xi,xj)K(\mathbf{x}_i, \mathbf{x}_j) を使って、高次元空間での内積 ϕ(xi)Tϕ(xj)\phi(\mathbf{x}_i)^T \phi(\mathbf{x}_j) を効率的に計算する「カーネルトリック」を用いるアプローチです。これにより、元の空間では非線形な決定境界を、写像先の高次元空間では線形な境界として学習することができます。カーネルパーセプトロンや、特に**サポートベクターマシン(SVM)**がこの代表例であり、かつて非常に強力な分類手法として広く使われました。 利点: 高次元(無限次元も可)の特徴空間を扱える。適切なカーネルを選べば複雑な境界を学習可能。 欠点: カーネル関数の選択やパラメータ調整が必要。大規模データに対する計算コストが高い場合がある。

  3. モデルの多層化(Multi-layering): パーセプトロンやADALINEのような単純な線形ユニットを複数**層(Layer)に積み重ねてネットワークを構築し、モデル自体の表現力を高めようとするアプローチです。これが多層パーセプトロン(Multi-Layer Perceptron, MLP)であり、現代のニューラルネットワーク(Neural Network)**の基本的な考え方です。このアプローチが、最終的に今日の深層学習ブームへとつながっていきます。

6.3 多層パーセプトロン (MLP):非線形性を内包するネットワーク

MLPは、通常、以下の3種類の層から構成されます。

各層(隠れ層と出力層)は複数のニューロン(ユニット)から成り、通常、ある層のニューロンは前の層の全てのニューロンと結合(全結合 Fully Connected)しています。

図6.2:典型的な多層パーセプトロン(MLP)の構造。入力層、1つの隠れ層、出力層が全結合されている。矢印は信号の流れと結合(重み)を表す。

非線形活性化関数:MLPの心臓部

MLPが単なる線形モデルの積み重ねに終わらず、真に強力な非線形表現力を獲得するための絶対的な鍵が、隠れ層(そして多くの場合、出力層でも)のニューロンに非線形の活性化関数 ϕ()\phi(\cdot) を適用することです。

もし、全てのニューロンが線形活性化関数(例:ϕ(z)=z\phi(z)=z)しか持たなければ、何層重ねようとも、入力から出力までの全体の変換は、結局一つの大きな線形変換(行列演算)で表現できてしまいます。つまり、表現力は単層の線形モデルと変わらないのです。

非線形な活性化関数を導入することで、層を重ねるごとに、より複雑で階層的な特徴表現を学習していくことが可能になります。代表的な非線形活性化関数には以下のようなものがあります。

普遍性定理 (Universal Approximation Theorem) 十分に多くの隠れニューロンを持つ1層または2層の隠れ層を持つMLPは、(活性化関数が適切であれば)任意の連続関数を望みの精度で近似できる、という強力な理論的結果があります。これは、MLPが原理的には非常に高い表現力を持つことを保証しています。

6.4 MLPの学習:誤差逆伝播法 (Backpropagation) の登場

MLPが理論的に強力でも、それをどうやって学習させるか、すなわち、大量の重みパラメータをデータに合わせてどう調整するか、が長年の難問でした。単層パーセプトロンの学習規則は、出力層の誤差しか直接扱えません。隠れ層のニューロンについては、直接的な教師信号(正解出力)がないため、その重みを更新するための誤差信号をどうやって計算すればよいのかが不明でした(信用割り当て問題 Credit Assignment Problem)。

この問題を解決し、MLPの学習を現実のものとしたのが、1970年代から研究され、1986年にデビッド・ラメルハート、ジェフリー・ヒントン、ロナルド・ウィリアムズによって効果的な形で再提示・普及された誤差逆伝播法(Error Backpropagation Algorithm)、通称**バックプロパゲーション(Backpropagation)**です5

バックプロパゲーションの核心アイデア: バックプロパゲーションは、ネットワーク全体のコスト関数 JJ (例:出力層での全サンプルの二乗誤差和やクロスエントロピー誤差)を定義し、そのコストに対する各重み wjkw_{jk} (層 kk のニューロン jj への入力に関する重み)の勾配(偏微分) Jwjk\frac{\partial J}{\partial w_{jk}} を効率的に計算するためのアルゴリズムです。これは、微積分の**連鎖律(Chain Rule)**を巧みに利用します。

アルゴリズムの流れ:

  1. 初期化: 全ての重みを小さな乱数値で初期化します。
  2. 反復: 訓練データを用いて以下のステップを繰り返します。 a. 順伝播 (Forward Propagation):
    • 入力サンプル x\mathbf{x} を入力層に与えます。
    • 信号を層ごとに順方向に伝播させます。各ニューロン jj は、前の層からの入力 aka_k と重み wjkw_{jk} を使って総入力 zj=kwjkakz_j = \sum_k w_{jk} a_k を計算し、活性化関数 ϕ(zj)\phi(z_j) を適用して自身の出力 aja_j を計算します。
    • これを最後の出力層まで繰り返し、最終的な予測値 y^\hat{\mathbf{y}} を得ます。
    • 予測値 y^\hat{\mathbf{y}} と正解ラベル y\mathbf{y} を用いて、コスト JJ を計算します。 b. 逆伝播 (Backward Propagation):
    • まず、出力層での誤差信号 δjout\delta_j^{\text{out}} を計算します。これは、コスト JJ の、出力層ニューロン jj の総入力 zjoutz_j^{\text{out}} に対する偏微分 Jzjout\frac{\partial J}{\partial z_j^{\text{out}}} に相当します。
    • 次に、この誤差信号 δjout\delta_j^{\text{out}} を、一つ前の隠れ層へ逆向きに伝播させます。隠れ層のニューロン kk の誤差信号 δkhidden\delta_k^{\text{hidden}} は、後段(出力層)のニューロン jj から受け取る誤差信号 δjout\delta_j^{\text{out}} を、それらを繋ぐ重み wjkw_{jk} で重み付けして合計し、さらに自身の活性化関数の微分 ϕ(zkhidden)\phi'(z_k^{\text{hidden}}) を掛け合わせることで計算されます(連鎖律の適用)。δk=(jwjkδj)ϕ(zk)\delta_k = (\sum_j w_{jk} \delta_j) \phi'(z_k)
    • この誤差信号の逆伝播を、入力層の直前の隠れ層まで繰り返します。 c. 勾配計算と重み更新:
    • 各層で計算された誤差信号 δj\delta_j を用いて、その層への入力に関する重み wjkw_{jk} の勾配 Jwjk\frac{\partial J}{\partial w_{jk}} を計算します。これは単純に Jwjk=akδj\frac{\partial J}{\partial w_{jk}} = a_k \delta_jaka_k は前層ニューロン kk の出力)となります。
    • 全ての重みの勾配が計算できたら、勾配降下法(通常はSGDまたはその変種)を用いて重みを更新します: wjk:=wjkηJwjkw_{jk} := w_{jk} - \eta \frac{\partial J}{\partial w_{jk}}
graph TD
    subgraph "MLP 学習 (Backpropagation + Gradient Descent)"
        direction LR
        Start --> Init["1. 重み初期化"]
        Init --> Forward["2a. 順伝播<br>(入力→出力, 予測と損失計算)"]
        Forward --> Backward["2b. 逆伝播<br>(出力層から誤差を逆伝播,<br>各層で誤差信号δ計算)"]
        Backward --> Gradient["2c. 勾配計算<br>(誤差信号δと前層出力aから<br>各重みの勾配 ∂J/∂w 計算)"]
        Gradient --> Update["2d. 重み更新<br>(勾配降下法で<br>w := w - η * ∂J/∂w)"]
        Update --> Check{"3. 終了条件?"}
        Check -- No --> Forward
        Check -- Yes --> End
    end

図6.3:誤差逆伝播法を用いたMLPの学習サイクルの概略。

バックプロパゲーションは、一見複雑に見えますが、連鎖律を体系的に適用することで、多数のパラメータを持つネットワークの勾配を効率的に計算できるエレガントなアルゴリズムです。このアルゴリズムの登場により、MLPは初めて実用的な学習能力を獲得し、パーセプトロンの限界を超えて、より複雑な非線形問題を解くための道が開かれました。

6.5 古典から深層学習へ:受け継がれる原理

今日の深層学習(Deep Learning)は、基本的には、多数の隠れ層を持つ非常に「深い」MLPや、畳み込み層(CNN)、再帰層(RNN)、Attention機構(Transformer)など、特定のタスクに合わせて特殊化した層構造を持つニューラルネットワークを指します。しかし、その学習の根幹にあるのは、やはりバックプロパゲーション勾配降下法(多くはその高度な変種であるAdamなど)です。

私たちがパーセプトロンとADALINEを通じて学んできた以下の基本的な概念は、そのまま深層学習の理解にも不可欠な要素として受け継がれています。

つまり、パーセプトロンとADALINEは、単なる歴史的な遺物ではなく、現代AIを理解するための基礎体力を養う上で、今なお重要な学習対象なのです。これらのシンプルなモデルを深く理解することで、より複雑な深層学習モデルの挙動や課題(勾配消失/爆発、過学習など)に対する洞察も深まります。


第7章:より深く理解するために ― 理論的背景と実践Tips

パーセプトロン、ADALINE、そしてMLPへと至る学習アルゴリズムの探求を通じて、私たちは機械学習の基本的なメカニズムに触れてきました。しかし、モデルを構築し、訓練し、評価するプロセスには、アルゴリズムの理解だけではカバーしきれない、より深い理論的背景や実践的な考慮事項が存在します。本章では、モデルの真の性能を理解し、より良い結果を得るために役立ついくつかの重要な概念とTipsを概観します。

7.1 汎化能力:モデルの真価は未知のデータで問われる

機械学習の究極的な目標は、過去のデータ(訓練データ)から学習した知識を使って、将来遭遇する未知のデータに対してもうまく予測・判断できるモデルを作ることです。この、未知のデータに対する性能を**汎化能力(Generalization Ability)**と呼びます。

訓練データに対する性能がいくら高くても、汎化能力が低ければそのモデルは役に立ちません。例えば、訓練データに存在する特定のノイズや偶然のパターンまで完璧に記憶してしまったモデルは、新しいデータが来たときにうまく対応できないでしょう。この状態を**過学習(Overfitting)**と呼びます。過学習したモデルは、訓練誤差(訓練データでの間違い)は非常に小さいのに、汎化誤差(未知データでの間違いの期待値)は大きい、という特徴を持ちます。

逆に、モデルが単純すぎて、訓練データに含まれる本質的な構造やパターンすら十分に捉えきれていない状態を**未学習(Underfitting)**と呼びます。未学習のモデルは、訓練誤差も汎化誤差も両方高いままです。

私たちが目指すべきは、未学習と過学習の間のちょうどよいバランスを見つけ、汎化誤差を最小にするモデルです。

図7.1:データ(点)に対するモデルの適合度の違い。左は未学習(単純すぎるモデル)、中は適切な学習、右は過学習(複雑すぎるモデルがノイズまで拾っている)。点線は訓練データでの性能、実線が真のパターンや汎化性能を表すイメージ。

どうやって汎化能力を測るか? 未知のデータでの性能を知りたいわけですが、通常、未来のデータは手元にありません。そこで、手持ちのデータを分割し、一部をモデルの訓練には使わずにとっておき、学習後のモデルの性能評価に使う、という方法が取られます。

この分割(特に検証/テストデータを使うこと)により、過学習を検知し、より汎化能力の高いモデルを選択することが可能になります。

7.2 モデルの複雑性:表現力と過学習リスクのトレードオフ

モデルの汎化能力は、その**複雑性(Complexity)表現力(Capacity / Representational Power)**と密接に関係しています。

モデルの複雑さを定量的に測る理論的な指標の一つとして、**VC次元(Vapnik-Chervonenkis dimension)**があります。これは、大雑把に言うと、モデルがどれだけ多くの異なるパターン(データ点の任意のラベル付け)を表現できるかを示す値です。

VC次元が大きい(モデルが複雑)ほど、そのモデルが訓練データをうまく説明するためには、より多くのサンプル数が必要になる傾向があります。統計的学習理論は、VC次元のような複雑性の尺度を用いて、訓練誤差と汎化誤差の間の関係(ギャップ)を理論的に解析し、汎化性能を保証するための条件(必要なサンプル数など)を導き出そうとします。

7.3 PAC学習理論:「たぶん、だいたい正しい」学習の保証

計算論的学習理論の枠組みであるPAC(Probably Approximately Correct)学習は、より形式的に「学習可能性」を議論します。PAC理論は、「ある問題クラスに対して、高い確率で(Probably)、望む精度で(Approximately Correct)、学習が成功するためには、どのような条件(アルゴリズムの存在、必要なサンプル数、計算時間)が必要か?」を問います。これにより、原理的に効率よく学習できる問題とそうでない問題を区別したり、アルゴリズムの性能を理論的に保証したりすることを目指します。例えば、パーセプトロンは線形分離可能な問題を(多項式時間で)PAC学習可能であることが知られています。

7.4 実装を成功させるためのTips:実践的なノウハウ集

理論的な理解も重要ですが、実際に機械学習モデルをうまく機能させるためには、多くの実践的なテクニックや経験則が役立ちます。ここでは、特にこれまでの議論に関連するものをいくつか再掲・補足します。

これらの理論的側面と実践的Tipsを理解し、適切に活用することが、単にアルゴリズムを知っているだけでなく、実際に問題を解決できる機械学習エンジニア・研究者になるための鍵となります。


エピローグ:古典に学び、未来を拓く ― AI探求の旅は続く

私たちは、本稿を通じて、半世紀以上前に生まれたパーセプトロンとADALINEという、機械学習のまさに「原点」を探る旅をしてきました。これらのアルゴリズムは、現代の深層学習の複雑さと比べれば、驚くほどシンプルに見えるかもしれません。しかし、そのシンプルさの中にこそ、今日のAIを駆動する普遍的な原理とアイデアが凝縮されています。

パーセプトロンは、脳の仕組みに触発され、「誤りから学ぶ」という画期的な学習規則を提示しました。それは、機械がデータを通じて自律的に知識を獲得する可能性を初めて具体的に示した、歴史的な一歩でした。その線形分離可能性という限界は、後の研究者たちに、より強力なモデルを探求する動機を与えました。

ADALINEは、学習を「コスト関数の最小化」という明確な最適化問題として定式化し、そのための強力なエンジンとして「勾配降下法」を導入しました。このパラダイムは、現代の機械学習・深層学習における標準的なアプローチの礎となっています。また、ADALINEの実践は、特徴量スケーリングのようなデータ前処理の重要性を私たちに教えてくれました。

これらの古典的アルゴリズムを深く、そして丁寧に学ぶことには、計り知れない価値があります。

パーセプトロンやADALINEは、AIという壮大なピラミッドを支える、決して目立つことはないかもしれませんが、不可欠な礎石です。その材質、形状、配置の原理を知ることなくして、ピラミッド全体の安定性や、さらなる高みを目指す可能性を真に理解することはできません。

そして、旅は続く…

この礎石の上に立ち、私たちはさらに広大なAIの世界を探求していくことができます。

AIと機械学習の探求は、終わりなき、そして刺激に満ちた旅です。その旅の中で、時折立ち止まり、この「原点」に立ち返ることは、進むべき方向を見定め、次なる一歩を踏み出すための力となるでしょう。本稿が、そのための信頼できる地図となり、皆さんの知的な冒険を力強く後押しできたなら、これに勝る喜びはありません。


付録:参考文献と推奨リソース

学習をさらに深めたい方のために、参考文献と役立つリソースをいくつか紹介します。

これらのリソースを活用し、さらに学びを深めていくことをお勧めします。


この記事は、提供された情報と一般的な機械学習の知識に基づき、AI言語モデルによって生成・再構成されました。図の参照元や具体的なファイルパスは、実際の環境に合わせてご確認ください。

Footnotes

  1. W. S. McCulloch and W. Pitts. “A Logical Calculus of the Ideas Immanent in Nervous Activity,” Bulletin of Mathematical Biophysics, 5(4): 115–133, 1943.

  2. F. Rosenblatt, “The Perceptron, A Perceiving and Recognizing Automaton.” Cornell Aeronautical Laboratory, Report 85-460-1, 1957. (より広く知られる論文は1958年のPsychological Review掲載)

  3. M. Minsky and S. Papert, Perceptrons: An Introduction to Computational Geometry. MIT Press, 1969.

  4. B. Widrow and M. E. Hoff, “Adaptive switching circuits,” 1960 IRE WESCON Convention Record, Part 4, pp. 96-104, 1960.

  5. D. E. Rumelhart, G. E. Hinton, and R. J. Williams. “Learning representations by back-propagating errors.” Nature, 323(6088):533–536, 1986.