線形代数や数値解析の講義でグラム・シュミットの正規直交化法を学習したが、数値計算の実装に苦労している。
考え方や実装方法を教えて欲しい。
こんなお悩みを解決します。
今回は、グラム・シュミットの正規直交化法の解説とともに、C言語による実装を紹介していきたいと思います。
数式を交えつつ、考え方を順番に解説していきますので、興味がある方はぜひ最後までご覧ください。
グラム・シュミットの正規直交化法の手順
まずは、『筑波大学の武内修 先生のサイト』を参考に手順を整理します。また、簡単のため実ベクトルのみを対象とします。
前提
ここでは、与えられた一次独立な\(n\)個のベクトル\(\boldsymbol{a}_{1}, \boldsymbol{a}_{2}, \cdots, \boldsymbol{a}_{n}\)から正規直交系\(\boldsymbol{e}_{1}, \boldsymbol{e}_{2}, \cdots, \boldsymbol{e}_{n}\)を構築します。この時、\(\forall k (1 \le k \le n)\)について、\(\boldsymbol{e}_{k}\)は、\(\boldsymbol{a}_{1}, \boldsymbol{a}_{2}, \cdots, \boldsymbol{a}_{n}\)の一次結合で表せるものとします。
正規直交化の手順
まず、\(\boldsymbol{e}_{1}\)を以下のように設定します。
$$ \boldsymbol{e}_{1} = \frac{1}{\| \boldsymbol{a}_{1} \|}\boldsymbol{a}_{1} \quad (1) $$
次に、\(2 \le k \le n\)において、\(\boldsymbol{e}_{k}\)を以下のように求めます。
$$
\begin{eqnarray}
\boldsymbol{e}_{k} & = & \frac{1}{\| \boldsymbol{f}_{k} \|}\boldsymbol{f}_{k} \quad (2) \\
\text{ where } \boldsymbol{f}_{k} & = & \boldsymbol{a}_{k} - \sum_{i = 1}^{k - 1}(\boldsymbol{a}_{k}, \boldsymbol{e}_{i})\boldsymbol{e}_{i} \quad (3)
\end{eqnarray}
$$
ここで、\(\| \cdot \|\)はユークリッドノルム、\((\boldsymbol{u}, \boldsymbol{v}) = \boldsymbol{u}^{\top}\boldsymbol{v}\)です。
例題
一次独立な3個のベクトル\(\boldsymbol{a}_{1}, \boldsymbol{a}_{2}, \boldsymbol{a}_{3}\)が以下のように与えられているとし、これらのベクトルを正規直交化します。
$$
\begin{eqnarray*}
\boldsymbol{a}_{1} & = & (1, 1, 0)^{\top} \\
\boldsymbol{a}_{2} & = & (1, 0, 2)^{\top} \\
\boldsymbol{a}_{3} & = & (2, 1, 3)^{\top}
\end{eqnarray*}
$$
\(\boldsymbol{e}_{1}\)の計算
\(\| \boldsymbol{a}_{1} \| = \sqrt{2}\)ですので、式(1)より\(\boldsymbol{e}_{1}\)は、
$$
\begin{eqnarray*}
\boldsymbol{e}_{1} & = & \frac{1}{ \| \boldsymbol{a}_{1} \| }\boldsymbol{a}_{1} \\
& = & \frac{1}{ \sqrt{2} }(1, 1, 0)^{\top} \\
& = & \left( \frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0 \right)^{\top}
\end{eqnarray*}
$$
となります。
\(\boldsymbol{e}_{2}\)の計算
\(k = 2\)の時の総和記号を展開すると、
$$
\boldsymbol{f}_{2} = \boldsymbol{a}_{2} - (\boldsymbol{a}_{2}, \boldsymbol{e}_{1})\boldsymbol{e}_{1}
$$
となります。ここで、
$$
\begin{eqnarray*}
(\boldsymbol{a}_{2}, \boldsymbol{e}_{1}) & = & (1, 0, 2)\left( \frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0 \right)^{\top} \\
& = & \frac{1}{\sqrt{2}}
\end{eqnarray*}
$$
ですので、\(\boldsymbol{f}_{2}\)は、
$$
\begin{eqnarray*}
\boldsymbol{f}_{2} & = & \boldsymbol{a}_{2} - (\boldsymbol{a}_{2}, \boldsymbol{e}_{1})\boldsymbol{e}_{1} \\
& = & (1, 0, 2)^{\top} - \frac{1}{\sqrt{2}}\left( \frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0 \right)^{\top} \\
& = & (1, 0, 2)^{\top} - \left( \frac{1}{2}, \frac{1}{2}, 0 \right)^{\top} \\
& = & \left( \frac{1}{2}, -\frac{1}{2}, 2 \right)^{\top}
\end{eqnarray*}
$$
となります。また、\(\| \boldsymbol{f}_{2} \| = \frac{3}{\sqrt{2}} \)です。よって、\(\boldsymbol{e}_{2}\)は、
$$
\begin{eqnarray*}
\boldsymbol{e}_{2} & = & \frac{1}{\| \boldsymbol{f}_{2} \|}\boldsymbol{f}_{2} \\
& = & \frac{\sqrt{2}}{3}\left( \frac{1}{2}, -\frac{1}{2}, 2 \right)^{\top} \\
& = & \left( \frac{\sqrt{2}}{6}, -\frac{\sqrt{2}}{6}, \frac{2\sqrt{2}}{3} \right)^{\top}
\end{eqnarray*}
$$
となります。
\(\boldsymbol{e}_{3}\)の計算
\(k = 3\)の時の総和記号を展開すると、
$$
\boldsymbol{f}_{3} = \boldsymbol{a}_{3} - (\boldsymbol{a}_{3}, \boldsymbol{e}_{1})\boldsymbol{e}_{1} - (\boldsymbol{a}_{3}, \boldsymbol{e}_{2})\boldsymbol{e}_{2}
$$
となります。ここで、
$$
\begin{eqnarray*}
(\boldsymbol{a}_{3}, \boldsymbol{e}_{1}) & = & (2, 1, 3)\left( \frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0 \right)^{\top} \\
& = & \frac{3}{\sqrt{2}} \\
(\boldsymbol{a}_{3}, \boldsymbol{e}_{2}) & = & (2, 1, 3)\left( \frac{\sqrt{2}}{6}, -\frac{\sqrt{2}}{6}, \frac{2\sqrt{2}}{3} \right)^{\top} \\
& = & \frac{13\sqrt{2}}{6}
\end{eqnarray*}
$$
ですので、\(\boldsymbol{f}_{3}\)は、
$$
\begin{eqnarray*}
\boldsymbol{f}_{3} & = & \boldsymbol{a}_{3} - (\boldsymbol{a}_{3}, \boldsymbol{e}_{1})\boldsymbol{e}_{1} - (\boldsymbol{a}_{3}, \boldsymbol{e}_{2})\boldsymbol{e}_{2} \\
& = & (2, 1, 3)^{\top} - \frac{3}{\sqrt{2}}\left( \frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0 \right)^{\top} - \frac{13\sqrt{2}}{6}\left( \frac{\sqrt{2}}{6}, -\frac{\sqrt{2}}{6}, \frac{2\sqrt{2}}{3} \right)^{\top} \\
& = & \left( 2 - \frac{3}{2} - \frac{13}{18}, 1 - \frac{3}{2} + \frac{13}{18}, 3 - \frac{26}{9} \right)^{\top} \\
& = & \left( -\frac{2}{9}, \frac{2}{9}, \frac{1}{9} \right)^{\top}
\end{eqnarray*}
$$
となります。また、\(\| \boldsymbol{f}_{3} \| = \frac{1}{3} \)です。よって、\(\boldsymbol{e}_{3}\)は、
$$
\begin{eqnarray*}
\boldsymbol{e}_{3} & = & \frac{1}{\| \boldsymbol{f}_{3} \|}\boldsymbol{f}_{3} \\
& = & 3\left( -\frac{2}{9}, \frac{2}{9}, \frac{1}{9} \right)^{\top} \\
& = & \left( -\frac{2}{3}, \frac{2}{3}, \frac{1}{3} \right)^{\top}
\end{eqnarray*}
$$
となります。
C言語による実装
実装結果を以下に格納しました。必要に応じて参照してください。
https://github.com/yuruto-free/gram-schmidt-orthonormalization
前提
引数として与えるベクトルのデータの構造は、以下のようになります。ここで、\(d\)は次元数、\(n\)はデータの数、\(\boldsymbol{a}_{k}=(a_{k, 1}, a_{k, 2}, \cdots, a_{k, d})^{\top}\)です。
$$
\begin{eqnarray*}
V = \left[
\begin{array}{cccc}
a_{1,1} & a_{1,2} & \cdots & a_{1,d}\\
a_{2,1} & a_{2,2} & \cdots & a_{2,d}\\
\vdots & \vdots & \ddots & \vdots \\
a_{n,1} & a_{n,2} & \cdots & a_{n,d}\\
\end{array}
\right]
\end{eqnarray*}
$$
すなわち、一次独立な\(n\)個のベクトル\(\boldsymbol{a}_{1}, \boldsymbol{a}_{2}, \cdots, \boldsymbol{a}_{n}\)を行ベクトル形式で並べたものが引数として与えるベクトルのデータ構造となります。
また、上記の変数名と関数の引数名の対応を以下に示します。
対象 | 変数名 | 関数の引数 |
---|---|---|
次元数 | \(d\) | dim |
ベクトルの数 | \(n\) | num |
ベクトル | \(V\) | vecs |
実装のコア部分
グラム・シュミットの正規直交化法を実現しているのは、以下の部分となります。
int32_t GS_orthonormalization(int32_t dim, int32_t num, double *vecs) {
int32_t ret = (int32_t)GS_NG;
int32_t func_val;
int32_t k;
double norm;
double *target;
double *work = NULL;
if (NULL != vecs) {
work = (double *)malloc(sizeof(double) * (num - 1));
if (NULL == work) {
goto EXIT_GS_ORTH;
}
target = &vecs[0];
/* e_{0}のノルムを計算 */
func_val = euclid_norm(dim, target, &norm);
if (((int32_t)RETURN_OK_GS != func_val) || (fabs(norm) < (double)MEPS_GS)) {
goto EXIT_GS_ORTH;
}
/* e_{0}を計算 */
func_val = scaling(dim, 1.0 / norm, target);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_GS_ORTH;
}
/* e_{1}からe_{n-1}を計算 */
for (k = 1; k < num; k++) {
target = &vecs[k * dim];
/* 直交ベクトルf_{k}を計算 */
func_val = orthogonal_vector(dim, k, work, vecs);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_GS_ORTH;
}
/* f_{k}のノルムを計算 */
func_val = euclid_norm(dim, target, &norm);
if (((int32_t)RETURN_OK_GS != func_val) ||
(fabs(norm) < (double)MEPS_GS)) {
goto EXIT_GS_ORTH;
}
/* e_{k}を計算 */
func_val = scaling(dim, 1.0 / norm, target);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_GS_ORTH;
}
}
ret = (int32_t)GS_OK;
}
EXIT_GS_ORTH:
if (NULL != work) {
free(work);
}
return ret;
}
// 中略
static int32_t orthogonal_vector(int32_t dim, int32_t idx, double *dots,
double *vecs) {
int32_t ret = (int32_t)RETURN_NG_GS;
int32_t i;
int32_t func_val;
double *target;
if ((NULL != dots) && (NULL != vecs)) {
target = &vecs[idx * dim];
/* 内積を計算 */
for (i = 0; i < idx; i++) {
func_val = inner_product(dim, target, &vecs[i * dim], &dots[i]);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_ORTH_VEC;
}
}
/* ベクトルの差を計算 */
for (i = 0; i < idx; i++) {
func_val = sub_vector(dim, dots[i], target, &vecs[i * dim]);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_ORTH_VEC;
}
}
ret = (int32_t)RETURN_OK_GS;
}
EXIT_ORTH_VEC:
return ret;
}
\(\boldsymbol{e}_{1}\)の計算
ベクトル\(V\)の1行目&vecs[0]
を取り出し、ノルムを計算・正規化すると\(\boldsymbol{e}_{1}\)が求まります。ここで、C言語の配列は0から始まるので、対応関係に注意してください。
target = &vecs[0];
/* e_{0}のノルムを計算 */
func_val = euclid_norm(dim, target, &norm);
if (((int32_t)RETURN_OK_GS != func_val) || (fabs(norm) < (double)MEPS_GS)) {
goto EXIT_GS_ORTH;
}
/* e_{0}を計算 */
func_val = scaling(dim, 1.0 / norm, target);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_GS_ORTH;
}
\(\boldsymbol{e}_{k}\)の計算
\(\boldsymbol{e}_{2}\)以降は、一般的な形で計算します。実装方法は、以下のようになります。
- \(\boldsymbol{a}_{k}\)と\(\boldsymbol{e}_{1}, \boldsymbol{e}_{2}, \cdots, \boldsymbol{e}_{k - 1}\)のそれぞれの内積を計算します。
- 得られた内積を係数として、式(3)を計算します。
- 式(2)を用いて、得られた\(\boldsymbol{f}_{k}\)を正規化します。
1.のステップの実行時、配列vecs
は以下のようになっています。
$$
\begin{eqnarray*}
\left[
\begin{array}{cccc}
e_{1,1} & e_{1,2} & \cdots & e_{1,d}\\
e_{2,1} & e_{2,2} & \cdots & e_{2,d}\\
\vdots & \vdots & \ddots & \vdots \\
e_{k - 1,1} & e_{k - 1,2} & \cdots & e_{k - 1,d}\\
a_{k,1} & a_{k,2} & \cdots & a_{k,d}\\
\end{array}
\right]
\end{eqnarray*}
$$
ここから、内積を計算する際は、作業用の配列を用意し、以下のように格納します。
$$
\begin{eqnarray*}
\boldsymbol{w} = \left[
\begin{array}{c}
(\boldsymbol{a}_{k}, \boldsymbol{e}_{1})\\
(\boldsymbol{a}_{k}, \boldsymbol{e}_{2})\\
\vdots \\
(\boldsymbol{a}_{k}, \boldsymbol{e}_{k - 1})\\
\end{array}
\right]
\end{eqnarray*}
$$
この結果を用いて、式(3)を計算します。ここで、上記の変数名と関数の引数の対応関係は以下のようになります。
対象 | 変数名 | 関数の引数名 |
---|---|---|
正規直交化対象 | \(k\) | idx |
内積 | \(\boldsymbol{w}\) | dots |
ベクトル | \(V\) | vecs |
static int32_t orthogonal_vector(int32_t dim, int32_t idx, double *dots,
double *vecs) {
int32_t ret = (int32_t)RETURN_NG_GS;
int32_t i;
int32_t func_val;
double *target;
if ((NULL != dots) && (NULL != vecs)) {
target = &vecs[idx * dim]; /* a_{k}に該当する変数 */
/* 内積を計算 */
for (i = 0; i < idx; i++) {
/* 内積 (a_{k}, e_{i}) を計算 */
func_val = inner_product(dim, target, &vecs[i * dim], &dots[i]);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_ORTH_VEC;
}
}
/* ベクトルの差を計算 */
for (i = 0; i < idx; i++) {
/* 式(3)の計算 */
func_val = sub_vector(dim, dots[i], target, &vecs[i * dim]);
if ((int32_t)RETURN_OK_GS != func_val) {
goto EXIT_ORTH_VEC;
}
}
ret = (int32_t)RETURN_OK_GS;
}
EXIT_ORTH_VEC:
return ret;
}
実行結果
main.cでは、以下のベクトルを正規直交化の対象としています。
$$
\begin{eqnarray*}
\left[
\begin{array}{ccc}
1 & 1 & 0\\
1 & 0 & 2\\
2 & 1 & 3\\
\end{array}
\right], \left[
\begin{array}{cccc}
1 & 1 & -2 & 2\\
0 & 1 & -1 & 0\\
3 & 5 & -2 & 1\\
\end{array}
\right], \left[
\begin{array}{ccccc}
1 & 2 & 1 & 3 & -1\\
0 & -2 & -3 & 3 & -2\\
2 & -3 & 0 & -4 & 3\\
3 & 1 & 2 & 3 & -4\\
\end{array}
\right]
\end{eqnarray*}
$$
テストコードを実行すると以下が得られます。少し誤差が含まれますが、おおむね計算できていることが分かります。
Dim: 3, Num: 3
[000] +0.70711 +0.70711 +0.00000
[001] +0.23570 -0.23570 +0.94281
[002] -0.66667 +0.66667 +0.33333
error: 4.1633363423443e-15
Dim: 4, Num: 3
[000] +0.31623 +0.31623 -0.63246 +0.63246
[001] -0.28604 +0.66742 -0.38139 -0.57208
[002] +0.67470 +0.51900 +0.51900 -0.07785
error: 3.6082248300318e-16
Dim: 5, Num: 4
[000] +0.25000 +0.50000 +0.25000 +0.75000 -0.25000
[001] -0.05000 -0.50000 -0.65000 +0.45000 -0.35000
[002] +0.85302 -0.36969 +0.06709 +0.05888 +0.35737
[003] +0.25084 -0.20149 +0.34284 -0.33565 -0.81626
error: 2.7384416402719e-03