4つの基本部分空間と零空間 — 行列を空間の言葉で読む

未知数が4つ、方程式が3本。この連立方程式は解けるでしょうか。「未知数のほうが多いから解は無数にある」と答えたくなりますが、実際にやってみると1つも解が無いことがあります。逆に、方程式のほうが多いのに解がぴったり1つ決まることもあります。式の本数と未知数の個数だけを数えても、解の個数はわからないのです。

では何を見れば良いのでしょうか。答えは「行列 $\bm{A}$ が空間をどう変形しているか」です。$\bm{A}$ は入力空間 $\mathbb{R}^n$ から出力空間 $\mathbb{R}^m$ への写像ですが、この2つの空間はそれぞれきれいに2つの部分空間に割れます。入力側は「意味のある方向」と「何をしても出力に現れない方向」に、出力側は「$\bm{A}$ で到達できる方向」と「絶対に到達できない方向」に。この合計4つの部分空間 — 列空間・行空間・零空間・左零空間 — を押さえると、連立方程式の可解性も、解の自由度も、最小二乗法の幾何も、すべて同じ1枚の絵から読み取れるようになります。

つまみ4個とメーター3個をもつ線形な機械の模式図。回してもメーターが動かないつまみの組み合わせが零空間、どう回しても作れないメーターの読みが左零空間にあたることを示す

行列を「つまみとメーターがついた機械」と見ると、4つの部分空間が何を測っているのかが一目でわかります。左下の赤い矢印が示すのは「回しても出力が1本も動かないつまみの組み合わせ」、つまり入力側で情報が潰れる方向です。右下の紫が示すのは「どう頑張っても針をそこに持っていけないメーターの読み」、つまり出力側の到達不能な方向です。この2つの”余り”が存在するかどうかが、そのまま解の個数を決めます。

この視点は応用の現場でそのまま効きます。ひとつは線形回帰と最小二乗法です。説明変数に多重共線性があると設計行列の零空間が非自明になり、係数が一意に決まりません。「なぜ係数が暴れるのか」「なぜリッジ回帰が効くのか」は零空間の言葉で説明できます。もうひとつはロボットアームの冗長自由度です。7自由度アームのヤコビアンの零空間は「手先を動かさずに肘だけを動かす関節速度」を表します。零空間が空でないからこそ、障害物を避けながら同じ位置を保てるわけです。ほかにも制御の可制御性・可観測性、CTスキャンの逆問題、圧縮センシングと、4つの部分空間は理工系のあちこちで顔を出します。

本記事の内容

  • 行列を「空間の写像」として見る直感と、4つの基本部分空間の定義
  • 階段形(RREF)の枢軸列と自由変数から rank + nullity = n を導出する
  • 行空間 $\perp$ 零空間、列空間 $\perp$ 左零空間という2組の直交補関係の証明
  • $\bm{A}\bm{x} = \bm{b}$ が解を持つ条件、一般解=特殊解+零空間の任意元という構造
  • 最小ノルム解が行空間にあることの導出と、Pythonでの実測

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

行列を「空間の写像」として見る

行列を「数字が並んだ表」だと思っているうちは、4つの部分空間の話は退屈な暗記になります。そこでまず、行列の見方を切り替えましょう。

工場に機械が1台あるとします。この機械にはつまみが $n$ 個ついていて、それぞれを好きな値に設定できます。設定値をまとめたものが入力ベクトル $\bm{x} \in \mathbb{R}^n$ です。機械の反対側にはメーターが $m$ 個あって、つまみの設定に応じて針が振れます。読み取り値をまとめたものが出力ベクトル $\bm{b} \in \mathbb{R}^m$ です。この機械の中身が線形であるとき、入出力の関係はちょうど行列の積 $\bm{b} = \bm{A}\bm{x}$ で書けます。

この機械を触っていると、2つの素朴な疑問が浮かびます。

疑問1: メーターの読みとして、どんな組み合わせでも作れるのか? つまみをどう回してもメーター2番だけを振らせることができない、という状況はあり得ます。「到達できるメーター読みの全体」が列空間です。$\bm{A}\bm{x}$ は $\bm{A}$ の列ベクトルの一次結合そのものですから、到達可能な出力の集合は列ベクトルが張る空間になります。

疑問2: 回してもメーターがまったく動かないつまみの回し方はあるか? つまみ1を $+2$、つまみ2を $-1$ だけ同時に動かすと、内部で効果が打ち消し合ってメーターが1本も動かない — そんな「無効な操作」が存在することがあります。この無効な操作の全体が零空間です。零空間が $\{\bm{0}\}$ より大きければ、同じ出力を与える入力が無数にあることになります。

この2つで入力側と出力側にひとつずつ部分空間ができました。残り2つはどこから来るのでしょうか。答えは転置です。$\bm{A}^\top$ に対して同じ2つの問いを立てると、$\bm{A}^\top$ の列空間(=$\bm{A}$ の行空間)と、$\bm{A}^\top$ の零空間(=$\bm{A}$ の左零空間)が現れます。しかも後で見るように、この転置側の2つは元の2つの「残り物」にぴったり収まります。行空間は零空間の直交補、列空間は左零空間の直交補になるのです。だから4つで打ち止めで、これらを4つの基本部分空間と呼びます。

ここまでは絵の話でした。次はこれを厳密な定義に落とします。

4つの基本部分空間の定義

$\bm{A}$ を $m \times n$ の実行列とします。行数 $m$ が出力空間の次元、列数 $n$ が入力空間の次元です。4つの部分空間は次のように定義されます。

列空間(column space)

$$ \begin{equation} C(\bm{A}) = \{ \bm{A}\bm{x} \mid \bm{x} \in \mathbb{R}^n \} \subseteq \mathbb{R}^m \end{equation} $$

$\bm{A}$ の列ベクトルを $\bm{a}_1, \dots, \bm{a}_n$ とすれば $\bm{A}\bm{x} = x_1 \bm{a}_1 + \dots + x_n \bm{a}_n$ ですから、$C(\bm{A})$ は列ベクトルが張る空間、つまり $\bm{A}$ のです。

零空間(null space, kernel)

$$ \begin{equation} N(\bm{A}) = \{ \bm{x} \in \mathbb{R}^n \mid \bm{A}\bm{x} = \bm{0} \} \subseteq \mathbb{R}^n \end{equation} $$

「出力を何も生まない入力」の集合です。とも呼びます。

行空間(row space)

$$ \begin{equation} C(\bm{A}^\top) = \{ \bm{A}^\top \bm{y} \mid \bm{y} \in \mathbb{R}^m \} \subseteq \mathbb{R}^n \end{equation} $$

$\bm{A}$ の行ベクトルを(縦ベクトルとみなして)張った空間です。零空間と同じ $\mathbb{R}^n$ に住んでいることに注意してください。

左零空間(left null space)

$$ \begin{equation} N(\bm{A}^\top) = \{ \bm{y} \in \mathbb{R}^m \mid \bm{A}^\top \bm{y} = \bm{0} \} \subseteq \mathbb{R}^m \end{equation} $$

$\bm{A}^\top \bm{y} = \bm{0}$ の両辺を転置すると $\bm{y}^\top \bm{A} = \bm{0}^\top$ となり、$\bm{y}$ が $\bm{A}$ に左から掛かる形になります。だから「左」零空間です。

これら4つが本当に部分空間になっていることは、$\bm{0}$ を含むこと・和で閉じること・スカラー倍で閉じることを確かめれば済みます。零空間を例に取りましょう。まず $\bm{A}\bm{0} = \bm{0}$ なので $\bm{0} \in N(\bm{A})$ です。次に $\bm{x}_1, \bm{x}_2 \in N(\bm{A})$ と実数 $\alpha, \beta$ に対して、行列積の線形性から

$$ \bm{A}(\alpha \bm{x}_1 + \beta \bm{x}_2) = \alpha \bm{A}\bm{x}_1 + \beta \bm{A}\bm{x}_2 = \alpha \bm{0} + \beta \bm{0} = \bm{0} $$

となり、一次結合もまた零空間に属します。列空間についても $\bm{A}\bm{x}_1$ と $\bm{A}\bm{x}_2$ の一次結合が $\bm{A}(\alpha \bm{x}_1 + \beta \bm{x}_2)$ と書けるので同様です。つまり「線形写像だから部分空間になる」というだけの話で、難しいことは何もありません。

住んでいる空間を整理しておきましょう。$\mathbb{R}^n$ に住むのは行空間と零空間、$\mathbb{R}^m$ に住むのは列空間と左零空間です。この住み分けを間違えると議論が全部おかしくなるので、最初に頭に入れてください。「行空間の次元は $n$ ではなく $m$ 個の行が張るのだから $\mathbb{R}^m$ では?」と混乱しがちですが、行ベクトルは長さ $n$ なので $\mathbb{R}^n$ の元です。行の本数ではなく、行の長さが住所を決めます

定義が出そろったので、次は具体的な行列でこれらを実際に計算してみます。手を動かすと「4つが勝手に決まるのではなく、互いに強く縛り合っている」ことが見えてきます。

主役となる例題行列

以降ずっと使う $3 \times 4$ 行列を導入します。

$$ \bm{A} = \begin{pmatrix} 1 & 2 & 3 & 5 \\ 2 & 4 & 8 & 12 \\ 3 & 6 & 7 & 13 \end{pmatrix} $$

$m = 3$、$n = 4$ です。つまり「つまみ4個、メーター3個」の機械だと思ってください。

一見しただけでも怪しい構造が見えます。第2列は第1列のちょうど2倍です。つまり列同士に依存関係があり、4本の列が張る空間は4次元よりずっと小さくなりそうです。行についても、第1行を $\bm{r}_1$、第2行を $\bm{r}_2$、第3行を $\bm{r}_3$ とすると

$$ \bm{r}_2 – 2\bm{r}_1 = (0, 0, 2, 2), \qquad \bm{r}_3 – 3\bm{r}_1 = (0, 0, -2, -2) $$

となり、後者は前者の $-1$ 倍です。したがって

$$ -5\bm{r}_1 + \bm{r}_2 + \bm{r}_3 = \bm{0} $$

という関係が成り立ちます。実際に計算してみると $(-5 + 2 + 3,\ -10 + 4 + 6,\ -15 + 8 + 7,\ -25 + 12 + 13) = (0,0,0,0)$ で確かにゼロです。この係数 $(-5, 1, 1)$ こそが左零空間のベクトルなのですが、その意味は後ほど明らかにします。

いまは「行にも列にも余分な情報が入っている」ことだけ押さえておけば十分です。この余分さを機械的に取り除く手続きが、次に見る階段形への変形です。

階段形(RREF)に何が保存されるか

連立方程式を解くときに使う掃き出し法、つまり行基本変形を思い出しましょう。行の入れ替え、行の定数倍、ある行に他の行の定数倍を足す — この3種類の操作です。ガウスの消去法を階段形(row echelon form)まで進め、さらに枢軸を1に正規化して枢軸の上も掃き出したものが簡約行階段形(reduced row echelon form, RREF)です。詳しくは連立一次方程式とガウスの消去法を解説を参照してください。

ここで重要なのは、行基本変形が4つの部分空間に対して何を保存し、何を壊すかです。結論を先に言うと、

  • 零空間は変わらない
  • 行空間は変わらない
  • 列空間は変わる(が、次元と「どの列が独立か」は変わらない)

理由を見ていきます。行基本変形はすべて、左から可逆行列 $\bm{E}$ を掛ける操作として書けます。行の入れ替えは置換行列、定数倍は対角行列、行の加算は単位行列の非対角成分に値を入れた行列で、いずれも逆行列を持ちます。したがって RREF への変形全体もひとつの可逆行列 $\bm{E}$ で $\bm{R} = \bm{E}\bm{A}$ と表せます。

零空間の不変性を示します。$\bm{A}\bm{x} = \bm{0}$ ならば両辺に左から $\bm{E}$ を掛けて $\bm{E}\bm{A}\bm{x} = \bm{R}\bm{x} = \bm{0}$ です。逆に $\bm{R}\bm{x} = \bm{0}$ ならば、$\bm{E}$ が可逆なので左から $\bm{E}^{-1}$ を掛けて $\bm{A}\bm{x} = \bm{0}$ が出ます。両向きが言えたので $N(\bm{A}) = N(\bm{R})$ です。当たり前と言えば当たり前で、「同値変形で連立方程式を解く」という操作の正当性そのものです。

行空間の不変性も同じ論法です。$\bm{R} = \bm{E}\bm{A}$ の各行は $\bm{A}$ の行の一次結合なので、$\bm{R}$ の行空間は $\bm{A}$ の行空間に含まれます。逆に $\bm{A} = \bm{E}^{-1}\bm{R}$ から $\bm{A}$ の行は $\bm{R}$ の行の一次結合であり、逆向きの包含も成り立ちます。よって行空間は完全に一致します。

一方で列空間は保存されません。例えば $\bm{A} = \begin{pmatrix} 1 \\ 1 \end{pmatrix}$ を掃き出すと $\bm{R} = \begin{pmatrix} 1 \\ 0 \end{pmatrix}$ になりますが、前者の列空間は直線 $y = x$、後者は $x$ 軸で、明らかに別物です。ただし救いがあります。列の間の一次従属関係、つまり「どの列がどの列の何倍の和で書けるか」は $\bm{E}$ を掛けても変わりません。$\bm{A}\bm{c} = \bm{0}$ と $\bm{R}\bm{c} = \bm{0}$ が同値だからです。したがって「$\bm{R}$ で独立な列の番号」を読み取り、その番号の元の $\bm{A}$ の列を取れば、$C(\bm{A})$ の基底が得られます。この「番号だけを借りてくる」操作が、列空間の基底を求める標準手順です。

保存されるもの・されないものが分かったので、実際に例題行列を階段形まで持っていきましょう。

枢軸列と自由変数

$\bm{A}$ を掃き出します。まず第1行を軸にして第2行・第3行の第1成分を消します。$\bm{r}_2 \leftarrow \bm{r}_2 – 2\bm{r}_1$、$\bm{r}_3 \leftarrow \bm{r}_3 – 3\bm{r}_1$ とすると

$$ \begin{pmatrix} 1 & 2 & 3 & 5 \\ 0 & 0 & 2 & 2 \\ 0 & 0 & -2 & -2 \end{pmatrix} $$

第2列は第2行以下がすべて0になってしまったので、枢軸は第2列を飛ばして第3列に移ります。第2行を $1/2$ 倍して $(0,0,1,1)$ にし、それを使って第3行を消すと $\bm{r}_3 \leftarrow \bm{r}_3 + 2 \times (0,0,1,1) = (0,0,0,0)$ です。最後に第1行から枢軸の上を掃き出します。$\bm{r}_1 \leftarrow \bm{r}_1 – 3 \times (0,0,1,1) = (1,2,0,2)$ として、

$$ \bm{R} = \begin{pmatrix} 1 & 2 & 0 & 2 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 0 \end{pmatrix} $$

が得られます。これが RREF です。各行の先頭に立つ1を枢軸(pivot)、それが立つ列を枢軸列と呼びます。ここでは第1列と第3列が枢軸列、第2列と第4列が自由列です。

例題行列を掃き出して簡約行階段形に変形する3段階の図。第2列は下がすべて0になるため枢軸が第3列へ飛び、枢軸2本・自由列2本からランク2と零空間の次元2が読み取れる

3枚の行列を並べると、掃き出しが何を暴き出しているのかがよく見えます。中央の段階で第2列の下側がすべて0になった瞬間に「第2列は前の列に従属している」ことが確定し、枢軸は第3列へ飛びます。右端の RREF では青く塗った第1列・第3列が枢軸列、残る2列が自由列で、この本数の内訳(2 + 2 = 4)がそのままランクと退化次数の内訳になっています。

枢軸の個数がランクです。ここでは $r = \operatorname{rank}\bm{A} = 2$ となります。この $r$ が、4つの部分空間の次元をすべて支配します。

  • $\dim C(\bm{A}^\top) = r$ — RREF の非零行 $(1,2,0,2)$ と $(0,0,1,1)$ が行空間の基底になります。この2本は「一方の枢軸位置で他方が0」という形をしているので明らかに一次独立で、行基本変形が行空間を保存するので $\bm{A}$ の行空間そのものを張ります。
  • $\dim C(\bm{A}) = r$ — 枢軸列の番号(第1列、第3列)に対応する $\bm{A}$ の列 $(1,2,3)^\top$ と $(3,8,7)^\top$ が列空間の基底です。

ここでさりげなく重要なことが起きています。行空間の次元と列空間の次元がどちらも枢軸の個数 $r$ に等しい、つまり「行ランク=列ランク」が、階段形を1回作るだけで同時に出てしまうのです。行と列という見た目にはまったく別の勘定が同じ数になるのは、階段形という共通の骨格を経由しているからです。

さて、枢軸に対応する変数(ここでは $x_1, x_3$)を枢軸変数、対応しない変数($x_2, x_4$)を自由変数と呼びます。名前のとおり、自由変数は好きな値を取れて、それを決めると枢軸変数が自動的に決まります。この「自由に決められる方向の個数」が、そのまま零空間の次元になります。次の節でその構成を厳密に行いましょう。

零空間の基底を「特殊解」で構成する

$\bm{A}\bm{x} = \bm{0}$ は $\bm{R}\bm{x} = \bm{0}$ と同値なので、RREF から方程式を読み取ります。

$$ \begin{cases} x_1 + 2x_2 + 2x_4 = 0 \\ x_3 + x_4 = 0 \end{cases} $$

枢軸変数について解くと、

$$ x_1 = -2x_2 – 2x_4, \qquad x_3 = -x_4 $$

です。ここで自由変数 $(x_2, x_4)$ に単位ベクトルを順番に代入します。これが基底を作る定石です。

$(x_2, x_4) = (1, 0)$ とすると $x_1 = -2$、$x_3 = 0$ なので

$$ \bm{n}_1 = (-2, 1, 0, 0)^\top $$

$(x_2, x_4) = (0, 1)$ とすると $x_1 = -2$、$x_3 = -1$ なので

$$ \bm{n}_2 = (-2, 0, -1, 1)^\top $$

自由変数(x2,x4)に(1,0)と(0,1)を順に代入して零空間の基底n1とn2を作る手順の図。枢軸変数は青、自由変数は赤で示され、どちらもAをかけるとゼロベクトルになる

図の赤いマスが自由変数、青いマスが枢軸変数です。赤いマスに単位ベクトルを立てると、青いマスの値は RREF の関係式から自動的に埋まる — それだけの作業で零空間の基底が2本できあがります。赤いマスの並びが単位行列になっているので一次独立性は見た目で確定し、青いマスが一意に決まることが「これだけで零空間全体を張る」ことの根拠になります。

この $\bm{n}_1, \bm{n}_2$ を特殊解と呼びます。念のため検算すると、$\bm{A}\bm{n}_1$ の第1成分は $1\cdot(-2) + 2\cdot 1 + 3 \cdot 0 + 5 \cdot 0 = 0$、第2成分は $2\cdot(-2)+4\cdot 1 = 0$、第3成分は $3\cdot(-2)+6\cdot 1 = 0$ で確かにゼロベクトルです。

これらが零空間の基底であることを2段階で示します。

(a) 一次独立である。 $c_1 \bm{n}_1 + c_2 \bm{n}_2 = \bm{0}$ とします。第2成分(自由変数 $x_2$ の位置)を見ると $\bm{n}_1$ は1、$\bm{n}_2$ は0なので、この成分の式は $c_1 = 0$ です。同様に第4成分(自由変数 $x_4$ の位置)を見ると $c_2 = 0$ が出ます。よって一次独立です。一般に、自由変数の座標だけを取り出すと特殊解たちは単位行列になるので、この議論はいつでも通ります。

(b) 零空間全体を張る。 任意の $\bm{x} \in N(\bm{A})$ を取り、その自由変数成分を $(x_2, x_4) = (\alpha, \beta)$ とします。ここで $\bm{z} = \bm{x} – \alpha \bm{n}_1 – \beta \bm{n}_2$ を考えます。零空間は部分空間なので $\bm{z} \in N(\bm{A})$ です。$\bm{z}$ の自由変数成分は構成上ちょうど $(\alpha – \alpha, \beta – \beta) = (0,0)$ になります。ところが RREF の関係式 $x_1 = -2x_2 – 2x_4$、$x_3 = -x_4$ は「自由変数が0なら枢軸変数も0」を意味するので、$\bm{z}$ は全成分0、すなわち $\bm{z} = \bm{0}$ です。よって $\bm{x} = \alpha \bm{n}_1 + \beta \bm{n}_2$ と書けました。

以上より $\dim N(\bm{A}) = 2$ で、基底は $\{\bm{n}_1, \bm{n}_2\}$ です。この構成は例題に特有のものではなく、自由変数が $k$ 個あれば同じ手順で $k$ 本の一次独立な特殊解が得られ、それが零空間を張ります。この一般性が、次に述べる定理の証明そのものになっています。

ランク・退化次数定理の導出

いま得られた事実を一般の $m \times n$ 行列に対して書き下すと、次の定理になります。

$$ \begin{equation} \operatorname{rank}\bm{A} + \dim N(\bm{A}) = n \end{equation} $$

$\dim N(\bm{A})$ を退化次数(nullity)と呼ぶので、rank + nullity = n と覚えられます。「$n$」が列数(入力空間の次元)であって行数ではない点に注意してください。

証明は、いままでの議論をそのまま繰り返すだけです。$\bm{A}$ を RREF に変形して $\bm{R}$ を得たとします。$\bm{R}$ の枢軸の個数を $r$ とすると、定義から $\operatorname{rank}\bm{A} = r$ です。列は全部で $n$ 本あり、そのうち $r$ 本が枢軸列なので、自由列は $n – r$ 本です。前節の (a)(b) により、自由変数1個につき特殊解が1本作られ、それらは一次独立で零空間を張ります。したがって

$$ \dim N(\bm{A}) = (\text{自由変数の個数}) = n – r $$

となり、移項すれば定理が得られます。

この式の読み方を、機械のアナロジーに戻して味わっておきましょう。つまみは全部で $n$ 個あります。そのうち $r$ 個分の「方向」は本当に出力を動かす有効な操作で、残り $n – r$ 個分の方向は何をしても出力に現れない無駄な操作です。ランクは有効な自由度、退化次数は無駄な自由度で、両者を足すと入力の総自由度 $n$ になる — これが定理の意味です。

さらに $\bm{A}^\top$ に対して同じ定理を適用します。$\bm{A}^\top$ は $n \times m$ 行列で、そのランクは行ランク=列ランクの一致から同じ $r$ ですから、

$$ \begin{equation} r + \dim N(\bm{A}^\top) = m \quad \Longrightarrow \quad \dim N(\bm{A}^\top) = m – r \end{equation} $$

となります。これで4つの次元がすべて $r$ で表せました。

部分空間 記号 住む空間 次元
列空間 $C(\bm{A})$ $\mathbb{R}^m$ $r$
左零空間 $N(\bm{A}^\top)$ $\mathbb{R}^m$ $m – r$
行空間 $C(\bm{A}^\top)$ $\mathbb{R}^n$ $r$
零空間 $N(\bm{A})$ $\mathbb{R}^n$ $n – r$

ランク2の例題行列について4つの部分空間の次元を帯グラフで示した図。入力空間は行空間2+零空間2=4、出力空間は列空間2+左零空間1=3で埋まる

帯グラフにすると、ランク $r$ が4つの数字すべてを支配していることが一目で分かります。上下どちらの帯も左半分の長さ(=ランク)が同じで、これが「行ランク=列ランク」に対応します。残りの長さが零空間・左零空間の次元で、ランクが1増えれば右側がその分だけ縮む、というトレードオフとして読めます。

例題では $m=3$、$n=4$、$r=2$ なので、列空間2次元・左零空間1次元・行空間2次元・零空間2次元です。$\mathbb{R}^3$ の側で $2 + 1 = 3$、$\mathbb{R}^4$ の側で $2 + 2 = 4$ と、それぞれきれいに勘定が合っています。

次元の合計が空間の次元に一致するというのは、ただの偶然ではありません。この2組はそれぞれ直交補の関係にあり、空間を余すところなく直交分解しているのです。次にそれを証明します。

行空間と零空間は直交補である

まず直交する(内積が0になる)ことを示し、その後で「補」まで言えることを次元の勘定で示します。

$\bm{x} \in N(\bm{A})$ を取ります。$\bm{A}\bm{x} = \bm{0}$ を成分で書くと、$\bm{A}$ の第 $i$ 行を $\bm{a}_i^\top$ として

$$ (\bm{A}\bm{x})_i = \bm{a}_i^\top \bm{x} = \langle \bm{a}_i, \bm{x} \rangle = 0 \quad (i = 1, \dots, m) $$

です。つまり「$\bm{A}\bm{x} = \bm{0}$」とは「$\bm{x}$ がすべての行ベクトルと直交する」という主張そのものです。行列とベクトルの積を「行と入力の内積を並べたもの」と読めば、これは定義を言い換えただけだと分かります。

行空間の任意の元は行ベクトルの一次結合 $\bm{v} = \sum_i c_i \bm{a}_i$ ですから、内積の線形性から

$$ \langle \bm{v}, \bm{x} \rangle = \left\langle \sum_i c_i \bm{a}_i, \bm{x} \right\rangle = \sum_i c_i \langle \bm{a}_i, \bm{x} \rangle = \sum_i c_i \cdot 0 = 0 $$

となり、$\bm{x}$ は行空間全体と直交します。よって $N(\bm{A}) \subseteq C(\bm{A}^\top)^\perp$ です。

逆向きも簡単です。$\bm{x} \in C(\bm{A}^\top)^\perp$ とすると、特に各行ベクトルと直交するので $\bm{a}_i^\top \bm{x} = 0$ がすべての $i$ で成り立ち、それを縦に並べれば $\bm{A}\bm{x} = \bm{0}$、つまり $\bm{x} \in N(\bm{A})$ です。両包含から

$$ \begin{equation} N(\bm{A}) = C(\bm{A}^\top)^\perp \end{equation} $$

が示されました。次元でも整合します。$\dim C(\bm{A}^\top)^\perp = n – r$ で、これは確かに $\dim N(\bm{A})$ に一致します。

この結果を分解の形で書き直すと、$\mathbb{R}^n$ は行空間と零空間の直交直和に分かれます。

$$ \begin{equation} \mathbb{R}^n = C(\bm{A}^\top) \oplus N(\bm{A}), \qquad C(\bm{A}^\top) \perp N(\bm{A}) \end{equation} $$

任意の入力 $\bm{x}$ は一意に $\bm{x} = \bm{x}_r + \bm{x}_n$($\bm{x}_r$ は行空間成分、$\bm{x}_n$ は零空間成分)と書けます。しかも $\bm{A}\bm{x} = \bm{A}\bm{x}_r + \bm{A}\bm{x}_n = \bm{A}\bm{x}_r$ なので、出力に効くのは行空間成分だけです。零空間成分はいくら足しても出力を変えません。ここが後で「解の一般形」と「最小ノルム解」を理解する鍵になります。

例題で確かめましょう。行空間の基底は $(1,2,0,2)$ と $(0,0,1,1)$、零空間の基底は $(-2,1,0,0)$ と $(-2,0,-1,1)$ でした。内積を全部計算すると、

$$ (1,2,0,2)\cdot(-2,1,0,0) = -2 + 2 = 0, \qquad (1,2,0,2)\cdot(-2,0,-1,1) = -2 + 0 + 0 + 2 = 0 $$

$$ (0,0,1,1)\cdot(-2,1,0,0) = 0, \qquad (0,0,1,1)\cdot(-2,0,-1,1) = -1 + 1 = 0 $$

4通りすべてゼロです。$\mathbb{R}^4$ の中に、互いに直交する2次元平面が2枚ぴったり収まっている、というのがこの行列の入力側の姿です。

同じ議論を転置に対して行えば、出力側の構造も決まります。次の節で確認します。

列空間と左零空間は直交補である

先ほどの結果を $\bm{A}$ ではなく $\bm{A}^\top$ に適用すれば、機械的に

$$ \begin{equation} N(\bm{A}^\top) = C(\bm{A})^\perp, \qquad \mathbb{R}^m = C(\bm{A}) \oplus N(\bm{A}^\top) \end{equation} $$

が得られます。ただ「転置すれば同じ」で済ませると印象に残らないので、直接の計算でも見ておきましょう。

$\bm{y} \in N(\bm{A}^\top)$ は $\bm{A}^\top \bm{y} = \bm{0}$、すなわち転置して $\bm{y}^\top \bm{A} = \bm{0}^\top$ を満たします。この式の第 $j$ 成分は $\bm{y}^\top \bm{a}_j = \langle \bm{y}, \bm{a}_j\rangle$($\bm{a}_j$ は $\bm{A}$ の第 $j$ )ですから、$\bm{y}$ はすべての列ベクトルと直交します。列の一次結合、つまり列空間の任意の元とも直交するので $\bm{y} \in C(\bm{A})^\perp$ です。逆も同様に辿れて、両者が一致します。

例題では左零空間は1次元で、その基底は先ほど見つけた関係式 $-5\bm{r}_1 + \bm{r}_2 + \bm{r}_3 = \bm{0}$ の係数

$$ \bm{y}_1 = (-5, 1, 1)^\top $$

です。列空間の基底 $(1,2,3)^\top$ と $(3,8,7)^\top$ に対して内積を取ると

$$ (-5,1,1)\cdot(1,2,3) = -5 + 2 + 3 = 0, \qquad (-5,1,1)\cdot(3,8,7) = -15 + 8 + 7 = 0 $$

でどちらもゼロです。$\mathbb{R}^3$ の中で、列空間は原点を通る平面、左零空間はその平面に垂直な直線(法線方向)になっています。左零空間ベクトルが「行の間の一次従属関係の係数」であることも見えてきます。$\bm{y}^\top \bm{A} = \bm{0}$ は「行ベクトルたちの一次結合がゼロになる」と書いてあるのですから、当然です。

行空間の基底と零空間の基底の内積がすべて0、列空間の基底と左零空間の基底の内積もすべて0であることを示す表。参考として行空間の基底どうしの内積は0でないことを併記

左と中央の表はすべて0(白)で、2組の直交補関係が実測でも成り立っていることを示しています。大事なのは右端の参考パネルで、同じ行空間の基底どうしで内積を取ると 9 や 2 といった非ゼロの値が出ます。つまり「内積を取ればたいてい0になる」わけではなく、行空間と零空間という組み合わせのときだけ0になる — 直交は偶然ではなく構造から出ている、ということです。

ここまでで4つの部分空間と2つの直交分解が出そろいました。この構造を1枚の図にすると、$\bm{A}$ という写像の全体像が見渡せます。

import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

fig, ax = plt.subplots(figsize=(10, 6))
ax.set_xlim(0, 10); ax.set_ylim(0, 7); ax.axis("off")

# 入力空間 R^n(左)と出力空間 R^m(右)の箱
ax.add_patch(plt.Rectangle((0.4, 0.5), 3.6, 6.0, fill=False, lw=2))
ax.add_patch(plt.Rectangle((6.0, 0.5), 3.6, 6.0, fill=False, lw=2))
ax.text(2.2, 6.75, "入力空間 $R^n$(つまみ側)", ha="center", fontsize=12)
ax.text(7.8, 6.75, "出力空間 $R^m$(メーター側)", ha="center", fontsize=12)

# 4つの部分空間
ax.add_patch(plt.Rectangle((0.7, 3.7), 3.0, 2.4, color="#4C72B0", alpha=0.35))
ax.text(2.2, 4.9, "行空間 $C(A^T)$\n次元 $r$", ha="center", va="center", fontsize=11)
ax.add_patch(plt.Rectangle((0.7, 0.9), 3.0, 2.4, color="#C44E52", alpha=0.35))
ax.text(2.2, 2.1, "零空間 $N(A)$\n次元 $n-r$", ha="center", va="center", fontsize=11)
ax.add_patch(plt.Rectangle((6.3, 3.7), 3.0, 2.4, color="#55A868", alpha=0.35))
ax.text(7.8, 4.9, "列空間 $C(A)$\n次元 $r$", ha="center", va="center", fontsize=11)
ax.add_patch(plt.Rectangle((6.3, 0.9), 3.0, 2.4, color="#8172B2", alpha=0.35))
ax.text(7.8, 2.1, "左零空間 $N(A^T)$\n次元 $m-r$", ha="center", va="center", fontsize=11)

# 写像の矢印
ax.annotate("", xy=(6.3, 4.9), xytext=(3.7, 4.9),
            arrowprops=dict(arrowstyle="-|>", lw=2.5, color="#333333"))
ax.text(5.0, 5.2, "$A$ は1対1で対応", ha="center", fontsize=11)
ax.annotate("", xy=(7.0, 3.55), xytext=(3.7, 2.1),
            arrowprops=dict(arrowstyle="-|>", lw=2.0, color="#C44E52", ls="--"))
ax.text(5.2, 2.5, "零空間は原点へ潰れる", ha="center", fontsize=10, color="#C44E52")
ax.text(7.8, 0.15, "左零空間には誰も到達できない", ha="center", fontsize=10, color="#8172B2")
ax.text(2.2, 3.5, "--- 互いに直交 ---", ha="center", fontsize=10)
ax.text(7.8, 3.5, "--- 互いに直交 ---", ha="center", fontsize=10)
plt.tight_layout(); plt.show()

入力空間と出力空間をそれぞれ2つの部分空間に分けた模式図。行空間と列空間が次元rで1対1に対応し、零空間は原点へ潰れ、左零空間には到達しないことを示す

この模式図が本記事の全体像です。左の箱(入力空間)は行空間と零空間に直交分解され、右の箱(出力空間)は列空間と左零空間に直交分解されます。$\bm{A}$ は行空間を列空間へ1対1で写し(どちらも次元 $r$ なので次元の辻褄が合います)、零空間は丸ごと原点へ潰し、左零空間には決して到達しません。「行列が情報を潰す方向」と「行列が届かない方向」が、それぞれ左下・右下の箱として明示されているのがポイントです。

なお「$\bm{A}$ が行空間を列空間へ1対1で写す」ことは証明できます。$\bm{x}_r, \bm{x}_r’ \in C(\bm{A}^\top)$ が $\bm{A}\bm{x}_r = \bm{A}\bm{x}_r’$ を満たすとすると、差 $\bm{d} = \bm{x}_r – \bm{x}_r’$ は行空間の元でありかつ $\bm{A}\bm{d} = \bm{0}$ すなわち零空間の元です。行空間と零空間は直交するので $\langle \bm{d}, \bm{d}\rangle = 0$、つまり $\bm{d} = \bm{0}$ となり $\bm{x}_r = \bm{x}_r’$ です。写像が全射であることは、任意の $\bm{b} = \bm{A}\bm{x}$ に対し $\bm{x} = \bm{x}_r + \bm{x}_n$ と分解して $\bm{b} = \bm{A}\bm{x}_r$ と書けることから従います。この「行空間と列空間の同型」が、後で扱う擬似逆行列の正体です。

構造が見えたところで、いよいよ本題の連立方程式に戻りましょう。

$\bm{A}\bm{x} = \bm{b}$ はいつ解けるのか

方程式 $\bm{A}\bm{x} = \bm{b}$ が解を持つ、という主張を空間の言葉で言い換えると一瞬で終わります。$\bm{A}\bm{x}$ が動き回る範囲はまさに列空間なので、

$$ \begin{equation} \bm{A}\bm{x} = \bm{b} \text{ が解を持つ} \iff \bm{b} \in C(\bm{A}) \end{equation} $$

です。しかしこの条件、そのままでは使いにくい。「$\bm{b}$ が列の一次結合で書けるか」を確かめるには結局また消去法が要ります。ところが前節の直交補関係 $C(\bm{A}) = N(\bm{A}^\top)^\perp$ を使うと、条件を直交性のチェックに変換できます。

$$ \begin{equation} \bm{b} \in C(\bm{A}) \iff \bm{b} \perp N(\bm{A}^\top) \iff \bm{y}^\top \bm{b} = 0 \ \ \forall \bm{y} \in N(\bm{A}^\top) \end{equation} $$

これはフレドホルムの二者択一(Fredholm alternative)と呼ばれる原理の有限次元版です。言葉にすると「$\bm{A}\bm{x} = \bm{b}$ が解を持つか、さもなくば $\bm{y}^\top\bm{A} = \bm{0}$ かつ $\bm{y}^\top \bm{b} \neq 0$ となる $\bm{y}$ が存在するか、どちらか一方だけが成り立つ」となります。

この定式化のありがたみは、矛盾の証拠が1本のベクトルで示せることです。もし $\bm{y}^\top \bm{A} = \bm{0}$ なのに $\bm{y}^\top \bm{b} \neq 0$ である $\bm{y}$ を見つけたら、$\bm{A}\bm{x} = \bm{b}$ の両辺に左から $\bm{y}^\top$ を掛けることで

$$ 0 = \bm{y}^\top \bm{A} \bm{x} = \bm{y}^\top \bm{b} \neq 0 $$

という矛盾が出ます。物理でいう保存則の破れを突きつけるようなもので、「その方程式は原理的に解けません」と一撃で言えるわけです。

例題に当てはめます。左零空間の基底は $\bm{y}_1 = (-5,1,1)^\top$ でしたから、可解条件はただ1本の線形制約

$$ -5b_1 + b_2 + b_3 = 0 $$

に集約されます。$\bm{b} = (1,2,3)^\top$ なら $-5 + 2 + 3 = 0$ なので解が存在します。一方 $\bm{b} = (1,0,0)^\top$ なら $-5 \neq 0$ なので解は1つもありません。$3$ 本の方程式で未知数 $4$ 個、素朴には「未知数過多だから必ず解ける」と思いたくなりますが、実際には $\mathbb{R}^3$ のうち2次元平面の上に $\bm{b}$ が乗っていなければならないのです。

この可解条件を図で確かめてみましょう。

import numpy as np
import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
y = np.array([-5., 1, 1])          # 左零空間の基底
c1, c2 = A[:, 0], A[:, 2]          # 列空間の基底(枢軸列)

fig = plt.figure(figsize=(7, 6))
ax = fig.add_subplot(111, projection="3d")
s = np.linspace(-1.2, 1.2, 12)
S, T = np.meshgrid(s, s)
P = S[..., None] * c1 + T[..., None] * c2      # 列空間の平面
ax.plot_surface(P[..., 0], P[..., 1], P[..., 2], alpha=0.30, color="#55A868")
ax.quiver(0, 0, 0, *(y / np.linalg.norm(y) * 4), color="#8172B2", lw=2.5,
          arrow_length_ratio=0.12)
ax.text(*(y / np.linalg.norm(y) * 4.4), "左零空間 $N(A^T)$", color="#8172B2")
for b, col, lab in [(np.array([1., 2, 3]), "#4C72B0", "b=(1,2,3): 平面上→解あり"),
                    (np.array([1., 0, 0]), "#C44E52", "b=(1,0,0): 平面外→解なし")]:
    ax.quiver(0, 0, 0, *b, color=col, lw=2.5, arrow_length_ratio=0.15)
    ax.text(*(b * 1.15), lab, color=col, fontsize=9)
ax.set_xlabel("$x_1$"); ax.set_ylabel("$x_2$"); ax.set_zlabel("$x_3$")
ax.set_title("列空間(緑の平面)と左零空間(紫の法線)")
ax.view_init(elev=22, azim=35)
plt.tight_layout(); plt.show()

3次元空間に描いた列空間の平面と、それに垂直な左零空間の矢印。b=(1,2,3)は平面上にあり解あり、b=(1,0,0)は平面から外れており解なしであることを示す

図を見ると、可解性の判定が幾何的にどれほど単純かがわかります。緑の平面が列空間で、紫の矢印がそれに垂直な左零空間です。$\bm{b} = (1,2,3)$ は平面に貼りついているので解があり、$\bm{b} = (1,0,0)$ は平面から浮いているので解がありません。「浮いている量」は法線方向の成分、すなわち $\bm{y}^\top\bm{b}$ の大きさで測れます。この浮いた分こそが、最小二乗法で「残差」と呼ばれるものの正体です。

解が存在すると分かったら、次はその解が何個あってどんな形をしているかです。

一般解 = 特殊解 + 零空間の任意元

$\bm{A}\bm{x} = \bm{b}$ の解をひとつ見つけたとします。それを特殊解 $\bm{x}_p$ と呼びます。このとき解全体の集合は

$$ \begin{equation} \{\bm{x} \mid \bm{A}\bm{x} = \bm{b}\} = \bm{x}_p + N(\bm{A}) = \{\bm{x}_p + \bm{n} \mid \bm{n} \in N(\bm{A})\} \end{equation} $$

となります。証明は両向きの包含です。まず $\bm{n} \in N(\bm{A})$ なら

$$ \bm{A}(\bm{x}_p + \bm{n}) = \bm{A}\bm{x}_p + \bm{A}\bm{n} = \bm{b} + \bm{0} = \bm{b} $$

なので $\bm{x}_p + \bm{n}$ は確かに解です。逆に $\bm{x}$ が任意の解なら、差を取って

$$ \bm{A}(\bm{x} – \bm{x}_p) = \bm{A}\bm{x} – \bm{A}\bm{x}_p = \bm{b} – \bm{b} = \bm{0} $$

より $\bm{x} – \bm{x}_p \in N(\bm{A})$、つまり $\bm{x} = \bm{x}_p + \bm{n}$ の形です。両包含が言えたので集合が一致します。

この構造は微分方程式の一般解=特殊解+斉次解とまったく同じ形をしています。線形性を持つ問題ではいつでもこの分解が現れる、と覚えておくと見通しが良くなります。

幾何的には、解集合は零空間を $\bm{x}_p$ だけ平行移動したアフィン部分空間です。零空間が原点を通る平面なら、解集合はその平面を平行移動した平面になります。原点を通らないので部分空間ではありませんが、形は完全に同じです。ここから重要な帰結が出ます。

  • $N(\bm{A}) = \{\bm{0}\}$ なら解は高々1個(存在すれば一意)
  • $N(\bm{A}) \neq \{\bm{0}\}$ なら、解は0個か無限個のどちらか

「解がちょうど2個」ということは線形方程式では起こりません。実数上の線形性がそれを許さないのです。

例題で具体的に書き下しましょう。$\bm{b} = (1,2,3)^\top$ は $\bm{A}$ の第1列そのものなので、特殊解として $\bm{x}_p = (1,0,0,0)^\top$ が即座に取れます。したがって一般解は

$$ \bm{x} = \begin{pmatrix} 1 \\ 0 \\ 0 \\ 0 \end{pmatrix} + c_1 \begin{pmatrix} -2 \\ 1 \\ 0 \\ 0 \end{pmatrix} + c_2 \begin{pmatrix} -2 \\ 0 \\ -1 \\ 1 \end{pmatrix}, \qquad c_1, c_2 \in \mathbb{R} $$

です。パラメータが2本あるので解は2次元分の無限集合、つまり $\mathbb{R}^4$ の中の平面です。この平面上のどの点を選んでも $\bm{A}\bm{x}$ は同じ $\bm{b}$ になります。

「どれを選んでも同じ」なら、何を基準に1つ選べば良いのでしょうか。自然な答えのひとつが「最も短いものを選ぶ」です。次節でそれが行空間と結びつくことを見ます。

import numpy as np
import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

# 3変数の縮約版で「一般解=特殊解+零空間」を可視化する
B = np.array([[1., 2, 3], [2, 4, 6], [1, 1, 1]])   # rank 2
n_vec = np.array([1., -2, 1])                       # N(B) の基底
b = B @ np.array([1., 0, 0])                        # 解が存在する右辺
xp = np.array([1., 0, 0])                           # 特殊解

t = np.linspace(-2.2, 2.2, 60)
line = xp[None, :] + t[:, None] * n_vec[None, :]     # 解集合(アフィン直線)
null_line = t[:, None] * n_vec[None, :]              # 零空間(原点を通る直線)

fig = plt.figure(figsize=(7, 6))
ax = fig.add_subplot(111, projection="3d")
ax.plot(*null_line.T, color="#C44E52", lw=2, ls="--", label="零空間 $N(A)$(原点を通る)")
ax.plot(*line.T, color="#4C72B0", lw=2.5, label="解集合 = 特殊解 + 零空間")
ax.scatter(*xp, color="#4C72B0", s=60)
ax.text(*(xp + 0.15), "特殊解 $x_p$", color="#4C72B0")
ax.scatter(0, 0, 0, color="k", s=40); ax.text(0.1, 0.1, -0.35, "原点")
ax.set_xlabel("$x_1$"); ax.set_ylabel("$x_2$"); ax.set_zlabel("$x_3$")
ax.set_title("解集合は零空間を平行移動したアフィン部分空間")
ax.legend(loc="upper left", fontsize=9); ax.view_init(elev=20, azim=-60)
plt.tight_layout(); plt.show()

原点を通る零空間の直線(赤い破線)と、それを特殊解の分だけ平行移動した解集合の直線(青い実線)を3次元で描いた図

赤い破線が零空間(原点を通る直線)、青い実線が解集合です。2本は完全に平行で、青いほうが特殊解 $\bm{x}_p$ の分だけ横にずれているだけだと見て取れます。青い直線上の点はどれも同じ $\bm{b}$ を生むので、方程式だけでは1点に絞れません。逆に言えば、原点にいちばん近い点を選ぶという追加の基準を入れれば、青い直線上の1点が自然に選ばれます。

最小ノルム解は行空間の中にある

解が無限にあるとき、実務では「いちばん素直な解」を1つ選びたくなります。素直さの定義として最も広く使われるのがノルム最小、つまり

$$ \min_{\bm{x}} \|\bm{x}\|_2 \quad \text{subject to} \quad \bm{A}\bm{x} = \bm{b} $$

です。ロボットの逆運動学なら「関節をなるべく動かさない解」、信号処理なら「エネルギー最小の入力」に相当します。この解が実は必ず行空間に乗るというのが、4つの部分空間の最も美しい帰結のひとつです。

導出しましょう。任意の解 $\bm{x}$ を直交分解 $\bm{x} = \bm{x}_r + \bm{x}_n$($\bm{x}_r \in C(\bm{A}^\top)$、$\bm{x}_n \in N(\bm{A})$)と書きます。直交するのでピタゴラスの定理が使えて、

$$ \|\bm{x}\|^2 = \|\bm{x}_r + \bm{x}_n\|^2 = \|\bm{x}_r\|^2 + 2\langle \bm{x}_r, \bm{x}_n \rangle + \|\bm{x}_n\|^2 $$

ここで直交性 $\langle \bm{x}_r, \bm{x}_n\rangle = 0$ を使うと中央の項が消えて、

$$ \|\bm{x}\|^2 = \|\bm{x}_r\|^2 + \|\bm{x}_n\|^2 $$

となります。一方、零空間成分は出力に影響しないので $\bm{A}\bm{x} = \bm{A}\bm{x}_r = \bm{b}$、つまり $\bm{x}_r$ だけでもすでに解です。したがってノルムを最小化するには $\|\bm{x}_n\|^2$ を最小、すなわち $\bm{x}_n = \bm{0}$ とすればよく、

$$ \begin{equation} \bm{x}^{+} = \bm{x}_r \in C(\bm{A}^\top) \end{equation} $$

が最小ノルム解です。しかも行空間成分は直交分解の一意性から一通りに決まるので、最小ノルム解はただ1つです。等号成立が $\bm{x}_n = \bm{0}$ のときに限ることから、最小性は狭義(他の解は必ず真に長い)です。

この $\bm{x}^{+}$ はムーア・ペンローズ擬似逆行列 $\bm{A}^{+}$ を使って $\bm{x}^{+} = \bm{A}^{+}\bm{b}$ と書けます。前節で見た「$\bm{A}$ は行空間と列空間の間の1対1対応を与える」という事実を思い出してください。$\bm{A}^{+}$ はまさにこの1対1対応の逆写像を、行空間・列空間の外側では0になるように拡張したものです。擬似逆行列の定義がなぜあの形なのかは、4つの部分空間の絵を見れば腑に落ちます。

例題で計算してみます。$\bm{b} = (1,2,3)^\top$ に対する最小ノルム解は

$$ \bm{x}^{+} = \frac{1}{7}(1, 2, -1, 1)^\top \approx (0.1429,\ 0.2857,\ -0.1429,\ 0.1429)^\top $$

で、ノルムは $\|\bm{x}^{+}\| = \sqrt{7}/7 = 1/\sqrt{7} \approx 0.3780$ です。これが本当に行空間の元かを確かめると、行空間の基底 $(1,2,0,2)$ と $(0,0,1,1)$ を使って

$$ \bm{x}^{+} = \frac{1}{7}(1,2,0,2) – \frac{1}{7}(0,0,1,1) = \frac{1}{7}(1, 2, -1, 1) $$

と書けています。一方、先ほどの特殊解 $\bm{x}_p = (1,0,0,0)^\top$ はノルム $1$ で、$\bm{x}^{+}$ よりずっと長い。実際 $\bm{x}_p$ の零空間成分は $\bm{x}_p – \bm{x}^{+} = \frac{1}{7}(6,-2,1,-1)^\top$ で、そのノルムの2乗は $(36+4+1+1)/49 = 6/7$ です。ピタゴラスの関係 $1 = 1/7 + 6/7$ がぴったり成り立っています。

解集合を零空間方向の座標(c1,c2)でパラメータ表示し、その上でのノルムの等高線を描いた図。中心の最小ノルム解0.3780が最小で、特殊解(1,0,0,0)はノルム1.0000の位置にある

この図は解集合($\mathbb{R}^4$ 内の平面)を零空間方向の座標 $(c_1, c_2)$ で真上から見たものです。平面上のどの点も $\bm{A}\bm{x} = \bm{b}$ を満たしますが、ノルムの等高線はきれいな同心円になり、中心に最小ノルム解 $\|\bm{x}^{+}\| = 0.3780$ が座っています。等高線が円になるのは、零空間の正規直交基底で測ればノルムがピタゴラスの式 $\|\bm{x}\|^2 = \|\bm{x}^{+}\|^2 + c_1^2 + c_2^2$ になるからです。特殊解 $\bm{x}_p$ は中心から離れた点にあり、その分だけノルムが 1.0000 まで膨らんでいます。

理論が出そろったので、ここからは Python で実際に手を動かし、いま導いた性質がすべて数値的に再現されることを確認します。

Pythonでの実装

1. RREF を自作して零空間の基底を構成する

まず、階段形への変形を自分で実装します。NumPy には RREF が無いので、部分ピボット選択つきのガウス・ジョルダン消去を書きます。これで枢軸列・自由列を取り出し、特殊解を並べて零空間の基底を作ります。

import numpy as np

def rref(M, tol=1e-12):
    """簡約行階段形(RREF)と枢軸列の番号を返す"""
    A = M.astype(float).copy()
    m, n = A.shape
    pivots, row = [], 0
    for col in range(n):
        if row >= m:
            break
        # 数値安定性のため絶対値最大の行を枢軸に選ぶ(部分ピボット選択)
        p = row + int(np.argmax(np.abs(A[row:, col])))
        if abs(A[p, col]) < tol:      # この列に枢軸は立たない = 自由列
            A[row:, col] = 0.0
            continue
        A[[row, p]] = A[[p, row]]     # 行を入れ替える
        A[row] = A[row] / A[row, col] # 枢軸を1に正規化
        for r in range(m):            # 枢軸の上下を掃き出す
            if r != row and abs(A[r, col]) > tol:
                A[r] -= A[r, col] * A[row]
        pivots.append(col)
        row += 1
    return A, pivots

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
R, piv = rref(A)
print("RREF =\n", R)
print("枢軸列 =", piv, " 自由列 =", [j for j in range(A.shape[1]) if j not in piv])

出力は RREF = [[1,2,0,2],[0,0,1,1],[0,0,0,0]]、枢軸列 [0, 2]、自由列 [1, 3] となり、手計算した階段形と完全に一致します。枢軸が2本なのでランクは2、自由列も2本なので退化次数も2で、rank + nullity = 2 + 2 = 4 = n が成立していることがこの1回の消去だけで読み取れます。

続いて、この RREF から特殊解を組み立てます。自由変数に単位ベクトルを順に代入する、という手計算の手順をそのままコードにします。

import numpy as np

def null_basis(M):
    """RREF から零空間の基底(特殊解を列に並べた行列)を作る"""
    R, piv = rref(M)
    n = M.shape[1]
    free = [j for j in range(n) if j not in piv]
    B = np.zeros((n, len(free)))
    for k, f in enumerate(free):
        B[f, k] = 1.0                     # 自由変数に 1 を立てる(他の自由変数は 0)
        for i, p in enumerate(piv):       # 枢軸変数を RREF の係数から決める
            B[p, k] = -R[i, f]
    return B

N_basis = null_basis(A)
print("零空間の基底(列)=\n", N_basis)
print("A @ N =\n", A @ N_basis)                 # ゼロ行列になるはず
print("左零空間の基底 =\n", null_basis(A.T))

零空間の基底として $(-2,1,0,0)^\top$ と $(-2,0,-1,1)^\top$ が返り、A @ N の成分は最大でも $10^{-15}$ 級、実質的にゼロ行列です。$\bm{A}^\top$ に同じ関数を適用すると $(-5,1,1)^\top$ が得られ、これは冒頭で見つけた行の一次従属関係 $-5\bm{r}_1 + \bm{r}_2 + \bm{r}_3 = \bm{0}$ の係数と一致します。左零空間が「行の間の従属関係を記録した空間」であることが実測で確認できました。

2. scipy と SVD の結果が一致することを確かめる

自作の基底と、scipy.linalg.null_space や SVD の右特異ベクトルから得られる基底は、見た目の数値がまったく違います(前者は整数ベクトル、後者は正規直交ベクトル)。しかし張る部分空間としては同一のはずです。基底そのものを比べても意味がないので、部分空間への直交射影行列を比べます。射影行列は基底の取り方によらず部分空間だけで決まるからです。

import numpy as np
from scipy.linalg import null_space, orth

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
r = np.linalg.matrix_rank(A)

U, S, Vt = np.linalg.svd(A)
print("特異値 =", np.round(S, 4))               # [22.9593, 1.6936, 0.0]

def proj(X):
    """列が張る部分空間への直交射影行列"""
    Q = orth(X)
    return Q @ Q.T

P_self  = proj(null_basis(A))     # 自作 RREF から
P_scipy = proj(null_space(A))     # scipy から
P_svd   = proj(Vt[r:].T)          # SVD の右特異ベクトル(r 番目以降)
print("||P_self - P_scipy|| =", np.linalg.norm(P_self - P_scipy))
print("||P_self - P_svd||   =", np.linalg.norm(P_self - P_svd))

特異値は $22.9593,\ 1.6936,\ 0$ で、非ゼロが2個ある — つまりランク2が特異値の側からも確認できます。そして3つの射影行列の差はいずれも $10^{-15}$ 程度、完全に機械精度の範囲です。RREF という代数的な手続きと、SVD という直交分解に基づく手続きが、まったく同じ部分空間を指していることが数値で裏づけられました。実務で零空間を求めるときは、丸め誤差に強い SVD 経由(null_space は内部で SVD を使います)が推奨されます。RREF は「どの成分が自由か」を明示できるので、手計算や教育目的で優れています。

特異値の並びから4つの部分空間の基底を一括で読む方法もまとめておきましょう。$\bm{A} = \bm{U}\bm{\Sigma}\bm{V}^\top$ とすると、$\bm{U}$ の最初の $r$ 列が列空間、残りが左零空間、$\bm{V}$ の最初の $r$ 列が行空間、残りが零空間の正規直交基底になります。SVD は4つの部分空間の直交基底を一度に吐き出す装置だと思うと、その位置づけが分かりやすくなります。詳しくは特異値分解(SVD)の導出と応用を参照してください。

3. 3次元で4つの部分空間を可視化する

$\mathbb{R}^4$ は描けないので、可視化用に $3 \times 3$ のランク落ち行列を使います。

$$ \bm{B} = \begin{pmatrix} 1 & 2 & 3 \\ 2 & 4 & 6 \\ 1 & 1 & 1 \end{pmatrix} $$

第2行は第1行の2倍なので $\operatorname{rank}\bm{B} = 2$、したがって零空間も左零空間も1次元です。行空間・列空間は平面、零空間・左零空間は直線になり、$\mathbb{R}^3$ の中に「平面と、それに垂直な直線」というペアが2組できます。

import numpy as np
import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

B = np.array([[1., 2, 3], [2, 4, 6], [1, 1, 1]])
row_basis = np.array([[1., 0, -1], [0., 1, 2]])   # 行空間(RREFの非零行)
n_vec     = np.array([1., -2, 1])                 # 零空間
col_basis = np.array([[1., 2, 1], [2., 4, 1]])    # 列空間(枢軸列)
yl_vec    = np.array([-2., 1, 0])                 # 左零空間
print("行空間・零空間の内積 =", row_basis @ n_vec)
print("列空間・左零空間の内積 =", col_basis @ yl_vec)

def draw(ax, basis, vec, c_plane, c_line, t_plane, t_line, title):
    s = np.linspace(-1.4, 1.4, 12)
    S, T = np.meshgrid(s, s)
    P = S[..., None] * basis[0] + T[..., None] * basis[1]
    ax.plot_surface(P[..., 0], P[..., 1], P[..., 2], alpha=0.32, color=c_plane)
    t = np.linspace(-2.4, 2.4, 2)
    L = t[:, None] * vec[None, :]
    ax.plot(*L.T, color=c_line, lw=3)
    ax.text(*(vec * 1.9), t_line, color=c_line, fontsize=10)
    ax.text(*(basis[0] * 1.5), t_plane, color=c_plane, fontsize=10)
    ax.set_title(title, fontsize=11)
    ax.set_xlabel("$x_1$"); ax.set_ylabel("$x_2$"); ax.set_zlabel("$x_3$")
    ax.view_init(elev=20, azim=40)

fig = plt.figure(figsize=(12, 5.5))
draw(fig.add_subplot(121, projection="3d"), row_basis, n_vec,
     "#4C72B0", "#C44E52", "行空間(平面)", "零空間(直線)",
     "入力空間 $R^3$:行空間 ⊥ 零空間")
draw(fig.add_subplot(122, projection="3d"), col_basis, yl_vec,
     "#55A868", "#8172B2", "列空間(平面)", "左零空間(直線)",
     "出力空間 $R^3$:列空間 ⊥ 左零空間")
plt.tight_layout(); plt.show()

ランク2の3×3行列Bについて、入力空間では行空間の平面と零空間の直線、出力空間では列空間の平面と左零空間の直線が直交している様子を並べた3D図

内積の出力はどちらも [0. 0.]、つまり直線は平面に含まれるどの方向とも直交しています。図でも、赤い直線が青い平面の法線方向をきれいに向いており、紫の直線が緑の平面の法線になっているのが見えます。$\mathbb{R}^3$ が「2次元の平面 + 1次元の直線」に直交分解される様子が、左右2枚でまったく同じ形で現れる — これが2組の直交補関係の視覚的な意味です。左の絵は入力側、右の絵は出力側で、行列 $\bm{B}$ は左の青い平面を右の緑の平面へ1対1で写し、赤い直線を原点に潰します。

次元の勘定も棒グラフにしておくと、ランクが4つの数字をすべて支配していることが一目で分かります。

import numpy as np
import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
m, n = A.shape
r = np.linalg.matrix_rank(A)

fig, ax = plt.subplots(figsize=(9, 3.4))
rows = [("入力空間 $R^4$", [("行空間 $C(A^T)$", r, "#4C72B0"),
                           ("零空間 $N(A)$", n - r, "#C44E52")], n),
        ("出力空間 $R^3$", [("列空間 $C(A)$", r, "#55A868"),
                           ("左零空間 $N(A^T)$", m - r, "#8172B2")], m)]
for i, (name, parts, total) in enumerate(rows):
    left = 0
    for label, w, c in parts:
        ax.barh(i, w, left=left, color=c, edgecolor="white", height=0.55)
        ax.text(left + w / 2, i, f"{label}\n次元 {w}", ha="center", va="center",
                fontsize=10, color="white")
        left += w
    ax.text(total + 0.12, i, f"合計 {total}", va="center", fontsize=11)
ax.set_yticks([0, 1]); ax.set_yticklabels([r[0] for r in rows], fontsize=11)
ax.set_xlim(0, 4.9); ax.set_xlabel("次元")
ax.set_title(f"ランク $r$={r} が4つの次元をすべて決める(rank + nullity = n)")
plt.tight_layout(); plt.show()

上段(入力空間 $\mathbb{R}^4$)は行空間2 + 零空間2 = 4、下段(出力空間 $\mathbb{R}^3$)は列空間2 + 左零空間1 = 3 と、それぞれの帯がぴったり空間の次元を埋めています。行空間と列空間の帯の長さが等しいことも視覚的に確認でき、これが「行ランク=列ランク」に対応します。ランクが1つ増えれば零空間と左零空間がその分だけ縮む、というトレードオフの関係も帯の伸び縮みとして読み取れます。

4. 最小ノルム解が行空間にあることを実測する

最後に、擬似逆行列が返す解が本当に行空間に乗っていて、しかも他のどの解よりも短いことを数値で確かめます。

import numpy as np
from scipy.linalg import null_space

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
b = np.array([1., 2, 3])                 # 左零空間 (-5,1,1) と直交 → 解あり
Nb = null_space(A)                       # 零空間の正規直交基底

x_min = np.linalg.pinv(A) @ b            # 最小ノルム解
x_p   = np.array([1., 0, 0, 0])          # 別の特殊解(Aの第1列がb)
print("最小ノルム解 =", np.round(x_min, 4), " ノルム =", np.linalg.norm(x_min))
print("残差 |Ax-b|  =", np.linalg.norm(A @ x_min - b))
print("零空間成分(0なら行空間内)=", np.round(Nb.T @ x_min, 12))
print("特殊解のノルム =", np.linalg.norm(x_p),
      " 零空間成分 =", np.round(Nb.T @ x_p, 4))

出力は最小ノルム解 $(0.1429, 0.2857, -0.1429, 0.1429)$、ノルム $0.3780$、残差はゼロです。零空間基底との内積は $10^{-16}$ 級で実質ゼロ、つまり $\bm{x}^{+}$ には零空間成分がまったく含まれていません。手計算で $\frac{1}{7}(1,2,-1,1)$ と求めた値と一致します。一方 $\bm{x}_p = (1,0,0,0)$ はノルム1で、零空間成分の内積が $(0.9123, 0.1576)$ と非ゼロです。「余分な零空間成分を持っている分だけ長い」という理論の主張がそのまま数値に出ています。

ランダムに解を生成して、ノルムの分布を最小ノルム解と比べてみましょう。

import numpy as np
from scipy.linalg import null_space
import matplotlib, matplotlib.pyplot as plt
for cand in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", "IPAexGothic", "Meiryo"]:
    if any(cand == f.name for f in matplotlib.font_manager.fontManager.ttflist):
        plt.rcParams["font.family"] = cand
        break
plt.rcParams["axes.unicode_minus"] = False

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
b = np.array([1., 2, 3])
Nb = null_space(A)
x_min = np.linalg.pinv(A) @ b

rng = np.random.default_rng(0)
C = rng.normal(scale=0.6, size=(4000, Nb.shape[1]))
X = x_min[None, :] + C @ Nb.T                  # 解集合から一様風にサンプリング
norms = np.linalg.norm(X, axis=1)
resid = np.linalg.norm(X @ A.T - b, axis=1)
print("残差の最大値 =", resid.max())            # どれも解になっている
print("最小ノルム解より短い解の個数 =", int((norms < np.linalg.norm(x_min)).sum()))

fig, ax = plt.subplots(figsize=(8, 4.2))
ax.hist(norms, bins=60, color="#4C72B0", alpha=0.8, label="解集合からサンプルした解のノルム")
ax.axvline(np.linalg.norm(x_min), color="#C44E52", lw=2.5,
           label=f"最小ノルム解 = {np.linalg.norm(x_min):.4f}")
ax.set_xlabel("$\\|x\\|_2$"); ax.set_ylabel("頻度")
ax.set_title("解はすべて $Ax=b$ を満たすが、最小ノルム解より短い解は存在しない")
ax.legend(fontsize=10)
plt.tight_layout(); plt.show()

解集合からサンプリングした4000個の解のノルムのヒストグラム。分布の左端に最小ノルム解0.3780の赤い縦線があり、それより短い解は1つもない

残差の最大値は $10^{-15}$ 程度で、サンプルした4000個すべてが正真正銘の解です。それでいて、最小ノルム解より短い解は0個でした。ヒストグラムでも赤い縦線が分布の完全な左端に位置しており、それより左には1本もバーが立ちません。零空間方向にどれだけ動いても方程式は満たされ続けるが、その分だけ必ずベクトルが長くなる — ピタゴラスの式 $\|\bm{x}\|^2 = \|\bm{x}_r\|^2 + \|\bm{x}_n\|^2$ が実測として見えている図です。

最後に、解が存在しない右辺に対して何が起きるかも見ておきます。$\bm{b} = (1,0,0)^\top$ は $\bm{y}_1^\top \bm{b} = -5 \neq 0$ なので解がありません。このとき lstsq は最小二乗解を返し、その残差ノルムは $\bm{b}$ の左零空間成分の大きさに一致するはずです。

import numpy as np

A = np.array([[1., 2, 3, 5], [2, 4, 8, 12], [3, 6, 7, 13]])
y = np.array([-5., 1, 1]) / np.linalg.norm([-5., 1, 1])   # 左零空間の単位ベクトル
b2 = np.array([1., 0, 0])                                  # 解なしの右辺

x_ls, *_ = np.linalg.lstsq(A, b2, rcond=None)
print("最小二乗解の残差ノルム =", np.linalg.norm(A @ x_ls - b2))
print("b の左零空間成分の大きさ =", abs(y @ b2))

解を持たない右辺b=(1,0,0)を列空間方向と左零空間方向に分解した図。列空間成分0.27217と左零空間成分0.96225が直角三角形をなし、残差ノルムが左零空間成分に一致することを示す

図は $\bm{b} = (1,0,0)$ を「列空間の方向」と「左零空間の方向」に分解したものです。緑の水平成分 $0.27217$ が最小二乗解の作れる出力、紫の垂直成分 $0.96225$ がどうやっても消せない残差で、両者は直角に交わります。$0.27217^2 + 0.96225^2 = 1 = \|\bm{b}\|^2$ とピタゴラスの式がぴったり閉じており、残差が「$\bm{b}$ の左零空間成分そのもの」であることが数値で確認できます。

残差ノルムは $0.96225$、左零空間成分 $|\bm{y}^\top \bm{b}_2| = 5/\sqrt{27} = 0.96225$ と小数点以下すべて一致します。これは偶然ではありません。最小二乗法は $\bm{b}$ を列空間へ直交射影する操作なので、取り除かれる残差はちょうど列空間の直交補=左零空間の成分になるからです。「解けない量」を測る物差しが左零空間だ、と言い換えてもよいでしょう。射影の詳細は射影行列と最小二乗法の理論で扱っています。

まとめ

本記事では、行列 $\bm{A}$ に付随する4つの基本部分空間を軸に、連立方程式の可解性と解の構造を空間の言葉で読み解きました。

  • 4つの基本部分空間 — 入力空間 $\mathbb{R}^n$ には行空間 $C(\bm{A}^\top)$ と零空間 $N(\bm{A})$、出力空間 $\mathbb{R}^m$ には列空間 $C(\bm{A})$ と左零空間 $N(\bm{A}^\top)$ が住みます。
  • rank + nullity = n — RREF の枢軸列が $r$ 本、自由列が $n-r$ 本。自由変数に単位ベクトルを代入して作る特殊解が零空間の基底になり、その本数が退化次数です。同じ階段形から行ランク=列ランクも同時に出ます。
  • 2組の直交補関係 — $\bm{A}\bm{x} = \bm{0}$ は「$\bm{x}$ が全行と直交する」ことなので $N(\bm{A}) = C(\bm{A}^\top)^\perp$、転置に同じ議論を適用して $N(\bm{A}^\top) = C(\bm{A})^\perp$。$\mathbb{R}^n$ も $\mathbb{R}^m$ も直交直和に分かれます。
  • 可解条件 — $\bm{A}\bm{x} = \bm{b}$ が解を持つのは $\bm{b} \in C(\bm{A})$、すなわち $\bm{b} \perp N(\bm{A}^\top)$ のとき。例題では左零空間ベクトル $(-5,1,1)$ から $-5b_1 + b_2 + b_3 = 0$ という1本の制約に集約されました。
  • 一般解の構造 — 解集合は $\bm{x}_p + N(\bm{A})$ というアフィン部分空間。解の個数は0個・1個・無限個の3択しかありません。
  • 最小ノルム解 — 直交分解とピタゴラスの定理から、ノルム最小の解は零空間成分を持たず、行空間の中の唯一の点になります。これが擬似逆行列 $\bm{A}^{+}\bm{b}$ の正体です。

この4つの部分空間の絵は、線形代数の先にあるほとんどの話題の土台になります。最小二乗法は「$\bm{b}$ を列空間へ射影する」操作であり、リッジ回帰は「零空間方向の暴れを抑える」正則化であり、特異値分解は「4つの部分空間の正規直交基底を一度に与える」分解です。制御工学の可制御性行列や可観測性行列も、結局はそのランクと零空間を見ています。行列を数字の表ではなく空間の写像として読む習慣がつけば、これらが全部同じ図の別の見方だと分かるようになります。

次のステップとして、以下の記事も参考にしてください。