からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

【Python】3.1:ユークリッドノルムの可視化【『スタンフォード線形代数入門』のノート】

はじめに

 『スタンフォード ベクトル・行列からはじめる最適化数学』の学習ノートです。
 「数式の行間埋め」や「Pythonを使っての再現」によって理解を目指します。本と一緒に読んでください。

 この記事は3.1節「ノルム」の内容です。
 ベクトルのユークリッドノルムを可視化します。

【前の内容】

www.anarchive-beta.com

【他の内容】

www.anarchive-beta.com

【今回の内容】

ノルムの可視化

 ユークリッドノルム(euclidean norm)をグラフで確認します。

 利用するライブラリを読み込みます。

# 利用ライブラリ
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


2次元の場合

 まずは、2次元空間(平面)上でベクトルのノルムを可視化します。

 2次元ベクトルを指定します。

# ベクトルを指定
x = np.array([3.0, 2.0])

  \mathbf{x} = (x_1, x_2)^{\top}xとして値を指定します。ただし、Pythonでは0からインデックスが割り当てられるので、 x_1の値はx[0]に対応します。

 ノルムを計算して、2次元空間上にベクトルを描画します。

# 作図用の値を設定
x_min = np.floor(np.min([0.0, x[0]])) - 1
x_max = np.ceil(np.max([0.0, x[0]])) + 1
y_min = np.floor(np.min([0.0, x[1]])) - 1
y_max = np.ceil(np.max([0.0, x[1]])) + 1

# ノルムを計算
norm_x = np.sqrt(np.sum(x**2))
print(norm_x)
norm_x = np.sqrt(np.dot(x, x))
print(norm_x)
norm_x = np.linalg.norm(x)
print(norm_x)

# 2Dベクトルを作図
fig, ax = plt.subplots(figsize=(6, 5), facecolor='white')
ax.scatter(*x, s=100, label='x') # 点x
ax.scatter(0, 0, s=100, label='O') # 原点
ax.quiver(0, 0, *x, angles='xy', scale_units='xy', scale=1) # ベクトルx
ax.annotate(xy=0.5*x, text='x', size=15, ha='right', va='bottom') # ベクトルxラベル
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.grid()
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$x=('+', '.join(map(str, x))+'), ' + 
             '\|x\|='+str(norm_x.round(2))+'$', loc='left')
fig.suptitle('norm x', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()
3.605551275463989
3.605551275463989
3.605551275463989

元のベクトル

 ベクトル \mathbf{x}axes.quiver()で描画します。第1・2引数に始点の座標、第3・4引数にベクトルのサイズ(変化量)を指定します。その他に、指定した値の通りに(調整せずに)矢印を描画するための設定をしています。
 配列xの前に*を付けてアンパック(展開)して指定しています。

 この例では、原点を始点とします。原点以外を始点とする場合については「距離の可視化」を参照してください。

 ユークリッドノルム |\mathbf{x}| = \sqrt{\sum_{i=1}^n x_i} = \sqrt{\mathbf{x}^{\top} \mathbf{x}}を計算して、タイトルに表示します。
 平方根はnp.sqrt()、内積はnp.dot()で計算できます。また、ノルムはnp.linalg.norm()でも計算できます。

 ベクトル \mathbf{x}を直角三角形の斜辺としたときの、3辺の関係を確認します。

# 2Dベクトルを作図
fig, ax = plt.subplots(figsize=(6, 5), facecolor='white')
ax.scatter(*x, s=100, label='x') # 点x
ax.scatter(0, 0, s=100, label='O') # 原点
ax.quiver(0, 0, *x, angles='xy', scale_units='xy', scale=1) # ベクトルx
ax.quiver(0, 0, x[0], 0, ec='black', linewidth=1.5, 
           scale_units='xy', scale=1, units='dots', width=0.1, headwidth=0.1) # 底辺
ax.quiver(x[0], 0, 0, x[1], ec='black', linewidth=1.5, 
          scale_units='xy', scale=1, units='dots', width=0.1, headwidth=0.1) # 高さ
ax.annotate(xy=0.5*x, text='$\sqrt{x_1^2+x_2^2}$', size=15, ha='right', va='bottom') # 斜辺ラベル
ax.annotate(xy=[0.5*x[0], 0], text='$x_1$', size=15, ha='center', va='top') # 底辺ラベル
ax.annotate(xy=[x[0], 0.5*x[1]], text='$x_2$', size=15, ha='left', va='center') # 高さラベル
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.grid()
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$x=('+', '.join(map(str, x))+'), ' + 
             '\|x\|='+str(norm_x.round(2))+'$', loc='left')
fig.suptitle('norm x', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルを斜辺とする三角形とノルムの関係

 底辺は x_1、高さは x_2なので、三平方の定理より斜辺が \sqrt{x_1^2 + x_2^2}になるのが分かります。

 ベクトルの値を変化させたアニメーションを作成します。

・作図コード(クリックで展開)

# フレーム数を設定
frame_num = 51

# ベクトルとして利用する値を指定
x_vals = np.array(
    [np.linspace(start=-6.0, stop=6.0, num=frame_num), 
     np.linspace(start=-4.0, stop=10.0, num=frame_num)]
).T

# 作図用の値を設定
x_min = np.floor(np.min([0.0, *x_vals[:, 0]])) - 1
x_max = np.ceil(np.max([0.0, *x_vals[:, 0]])) + 1
y_min = np.floor(np.min([0.0, *x_vals[:, 1]])) - 1
y_max = np.ceil(np.max([0.0, *x_vals[:, 1]])) + 1

# 作図用のオブジェクトを初期化
fig, ax = plt.subplots(figsize=(7, 7), facecolor='white')
fig.suptitle('norm x', fontsize=20)
    
# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のベクトルを取得
    x = x_vals[i]
    
    # ノルムを計算
    norm_x = np.linalg.norm(x)
    
    # 2Dベクトルを作図
    ax.scatter(*x, s=100, label='x') # 点x
    ax.scatter(0, 0, s=100, label='O') # 原点
    ax.quiver(0, 0, *x, angles='xy', scale_units='xy', scale=1) # ベクトルx
    ax.annotate(xy=0.5*x, text='x', size=15, ha='right', va='bottom') # ベクトルxラベル
    ax.set_xticks(ticks=np.arange(x_min, x_max+1))
    ax.set_yticks(ticks=np.arange(y_min, y_max+1))
    ax.grid()
    ax.set_xlabel('$x_1$')
    ax.set_ylabel('$x_2$')
    ax.set_title('$x=('+', '.join(map(str, x.round(2)))+'), ' + 
                 '\|x\|='+str(norm_x.round(2))+'$', loc='left')
    ax.legend()
    ax.set_aspect('equal')

# gif画像を作成
ani = FuncAnimation(fig=fig, func=update, frames=frame_num, interval=100)

# gif画像を保存
ani.save('norm_2d.gif')

 作図処理をupdate()として定義して、FuncAnimation()でgif画像を作成します。

2次元空間上のベクトルとノルムの関係


3次元の場合

 続いて、3次元空間上でベクトルのノルムを可視化します。

 3次元ベクトルを指定します。

# ベクトルを指定
x = np.array([4.0, 3.0, 2.0])

  \mathbf{x} = (x_1, x_2, x_3)^{\top}xとして値を指定します。

 ノルムを計算して、3次元空間上にベクトルを描画します。

# 作図用の値を設定
x_min = np.floor(np.min([0.0, x[0]])) - 1
x_max = np.ceil(np.max([0.0, x[0]])) + 1
y_min = np.floor(np.min([0.0, x[1]])) - 1
y_max = np.ceil(np.max([0.0, x[1]])) + 1
z_min = np.floor(np.min([0.0, x[2]])) - 1
z_max = np.ceil(np.max([0.0, x[2]])) + 1

# ノルムを計算
norm_x = np.sqrt(np.sum(x**2))
print(norm_x)
norm_x = np.sqrt(np.dot(x, x))
print(norm_x)
norm_x = np.linalg.norm(x)
print(norm_x)

# 3Dベクトルを作図
fig, ax = plt.subplots(figsize=(7, 7), facecolor='white', 
                       subplot_kw={'projection': '3d'})
ax.scatter(*x, s=100, label='x') # 点x
ax.scatter(0, 0, 0, s=100, label='O') # 原点
ax.quiver(0, 0, 0, *x, color='black', arrow_length_ratio=0.05) # ベクトルx
ax.text(*0.5*x, s='x', size=15, ha='right', va='bottom') # ベクトルxラベル
ax.quiver([0, x[0]], [0, x[1]], [z_min, z_min], 
          [0, 0], [0, 0], [-z_min, x[2]-z_min], 
          color='gray', arrow_length_ratio=0, linestyle=':') # 補助線
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_zticks(ticks=np.arange(z_min, z_max+1))
ax.set_zlim(z_min, z_max)
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_zlabel('$x_3$')
ax.set_title('$x=('+', '.join(map(str, x))+'), ' + 
             '\|x\|='+str(norm_x.round(2))+'$', loc='left')
fig.suptitle('norm x', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()
5.385164807134504
5.385164807134504
5.385164807134504

元のベクトル

 axes.quiver()の第1・2・3引数に始点の座標、第4・5・6引数にベクトルのサイズを指定します。

 「2次元の場合」と同じコードでノルムを計算できます。

 ベクトル \mathbf{x}を直角三角形の斜辺としたときの、3辺の関係を確認します。

# 3Dベクトルを作図
fig, ax = plt.subplots(figsize=(7, 7), facecolor='white', 
                       subplot_kw={'projection': '3d'})
ax.scatter(*x, s=100, label='x') # 点x
ax.scatter(0, 0, 0, s=100, label='O') # 原点
ax.quiver(0, 0, 0, *x, color='black', arrow_length_ratio=0.05) # ベクトルx
ax.quiver(0, 0, 0, x[0], x[1], 0, color='black', arrow_length_ratio=0.00) # 底辺の斜辺
ax.quiver([0, 0, 0, 0], [0, x[1], 0, x[1]], [0, 0, x[2], x[2]], 
          [x[0], x[0], x[0], x[0]], [0, 0, 0, 0], [0, 0, 0, 0], 
          color='gray', arrow_length_ratio=0.0) # 横幅
ax.quiver([0, x[0], 0, x[0]], [0, 0, 0, 0], [0, 0, x[2], x[2]], 
          [0, 0, 0, 0], [x[1], x[1], x[1], x[1]], [0, 0, 0, 0], 
          color='gray', arrow_length_ratio=0.0) # 奥行き
ax.quiver([0, x[0], 0, x[0]], [0, 0, x[1], x[1]], [0, 0, 0, 0], 
          [0, 0, 0, 0], [0, 0, 0, 0], [x[2], x[2], x[2], x[2]], 
          color='gray', arrow_length_ratio=0.0) # 高さ
ax.text(*0.5*x, s='$\sqrt{x_1^2 + x_2^2 + x_3^2}$', 
        size=10, ha='right', va='bottom') # 斜辺ラベル
ax.text(0.5*x[0], 0.5*x[1], 0, s='$\sqrt{x_1^2 + x_2^2}$', 
        size=10, ha='center', va='top') # 底辺の斜辺ラベル
ax.text(0.5*x[0], 0, 0, s='$x_1$', size=15, ha='center', va='top') # 横幅ラベル
ax.text(x[0], 0.5*x[1], 0, s='$x_2$', size=15, ha='left', va='center') # 奥行きラベル
ax.text(x[0], x[1], 0.5*x[2], s='$x_3$', size=15, ha='left', va='center') # 高さラベル
ax.quiver([0, x[0]], [0, x[1]], [z_min, z_min], 
          [0, 0], [0, 0], [-z_min, x[2]-z_min], 
          color='gray', arrow_length_ratio=0, linestyle=':') # 補助線
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_zticks(ticks=np.arange(z_min, z_max+1))
ax.set_zlim(z_min, z_max)
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_zlabel('$x_3$')
ax.set_title('$x=('+', '.join(map(str, x))+'), ' + 
             '\|x\|='+str(norm_x.round(2))+'$', loc='left')
fig.suptitle('norm x', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルを斜辺とする三角形とノルムの関係

 三平方の定理より、横幅が x_1で奥行きが x_2なので底辺上の斜辺が \sqrt{x_1^2 + x_2^2}であり、また高さが x_3なので直方体の対角線が \sqrt{x_1^2 + x_2^2 + x_3^2}になるのが分かります。

 ベクトルの値を変化させたアニメーションを作成します。

・作図コード(クリックで展開)

# フレーム数を設定
frame_num = 51

# ベクトルとして利用する値を指定
x_vals = np.array(
    [np.linspace(start=-6.0, stop=6.0, num=frame_num), 
     np.linspace(start=0.0, stop=10.0, num=frame_num), 
     np.linspace(start=-4.0, stop=3.0, num=frame_num)]
).T

# 作図用の値を設定
x_min = np.floor(np.min([0.0, *x_vals[:, 0]])) - 1
x_max = np.ceil(np.max([0.0, *x_vals[:, 0]])) + 1
y_min = np.floor(np.min([0.0, *x_vals[:, 1]])) - 1
y_max = np.ceil(np.max([0.0, *x_vals[:, 1]])) + 1
z_min = np.floor(np.min([0.0, *x_vals[:, 2]])) - 1
z_max = np.ceil(np.max([0.0, *x_vals[:, 2]])) + 1

# 作図用のオブジェクトを初期化
fig, ax = plt.subplots(figsize=(8, 8), facecolor='white', 
                       subplot_kw={'projection': '3d'})
fig.suptitle('norm x', fontsize=20)

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のベクトルを取得
    x = x_vals[i]
    
    # ノルムを計算
    norm_x = np.linalg.norm(x)
        
    # 3Dベクトルを作図
    ax.scatter(*x, s=100, label='x') # 点x
    ax.scatter(0, 0, 0, s=100, label='O') # 原点
    ax.quiver(0, 0, 0, *x, arrow_length_ratio=0.1, color='black') # ベクトルx
    ax.text(*0.5*x, s='x', size=15, ha='right', va='bottom') # ベクトルxラベル
    ax.quiver([x_min, x_min, 0, x[0], 0, x[0]], 
              [0, x[1], y_max, y_max, 0, x[1]], 
              [0, x[2], 0, x[2], z_min, z_min], 
              [-x_min, x[0]-x_min, 0, 0, 0, 0], 
              [0, 0, -y_max, x[1]-y_max, 0, 0], 
              [0, 0, 0, 0, -z_min, x[2]-z_min], 
              color='gray', arrow_length_ratio=0, linestyle=':') # 補助線
    ax.set_xticks(ticks=np.arange(x_min, x_max+1))
    ax.set_yticks(ticks=np.arange(y_min, y_max+1))
    ax.set_zticks(ticks=np.arange(z_min, z_max+1))
    ax.set_xlim(x_min, x_max)
    ax.set_ylim(y_min, y_max)
    ax.set_zlim(z_min, z_max)
    ax.set_xlabel('$x_1$')
    ax.set_ylabel('$x_2$')
    ax.set_zlabel('$x_3$')
    ax.set_title('$x=('+', '.join(map(str, x.round(2)))+'), ' + 
                 '\|x\|='+str(norm_x.round(2))+'$', loc='left')
    ax.legend()
    ax.set_aspect('equal')

# gif画像を作成
ani = FuncAnimation(fig=fig, func=update, frames=frame_num, interval=100)

# gif画像を保存
ani.save('norm_3d.gif')

3次元空間上のベクトルとノルムの関係


 この記事では、ユークリッドノルムを作図しました。次の記事では、ユークリッド距離を作図します。

参考書籍

  • Stephen Boyd・Lieven Vandenberghe(著),玉木 徹(訳)『スタンフォード ベクトル・行列からはじめる最適化数学』講談社サイエンティク,2021年.

おわりに

 1章の始めとほぼ同じ感じになってしまいました。まぁよく言うと、螺旋階段を一周したような感じですね?

 2023年3月12日は、モーニング娘。'23の小田さくらさんの24歳のお誕生日です♪

 

 生で聴いた「もののけ姫」が言葉にならないほど凄かった。歌声の魅力を最大限引き出すようなソロ曲はまだでしょうか。

【次の内容】

www.anarchive-beta.com