からっぽのしょこ

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

【Python】1.3:ベクトルのアフィン結合の可視化【『スタンフォード線形代数入門』のノート】

はじめに

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

 この記事は1.3節「ベクトルスカラー積」の内容です。
 ベクトルのアフィン結合を可視化します。

【前の内容】

www.anarchive-beta.com

【他の内容】

www.anarchive-beta.com

【今回の内容】

ベクトルのアフィン結合の可視化

 ベクトルのアフィン結合(affine combination)をグラフで確認します。アフィン結合は、係数の和が1になる場合のベクトルの線形結合です。
 任意の係数による線形結合については「www.anarchive-beta.com」を参照してください。

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

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


2次元の場合

 まずは、2次元空間(平面)上でベクトルのアフィン結合を可視化します。

 2次元ベクトルと係数(スカラー値)を指定します。

# ベクトルを指定
a = np.array([-3.0, 1.0])
b = np.array([2.0, 3.0])

# 係数を指定
theta = -0.4

  \mathbf{a} = (a_1, a_2)^{\top}, \mathbf{b} = (b_1, b_2)^{\top}a, b、係数 \thetathetaとして値を指定します。ただし、Pythonでは0からインデックスが割り当てられるので、 a_1の値はa[0]に対応します。

 2次元空間上に2つのベクトル \mathbf{a}, \mathbf{b}を描画します。

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

# (原点からの)2Dベクトルを作図
fig, ax = plt.subplots(figsize=(6, 5), facecolor='white')
ax.quiver(0, 0, *a, color='red', angles='xy', scale_units='xy', scale=1) # ベクトルa
ax.quiver(0, 0, *b, color='blue', angles='xy', scale_units='xy', scale=1) # ベクトルb
ax.annotate(xy=0.5*a, text='a', size=15, ha='center', va='top') # ベクトルaラベル
ax.annotate(xy=0.5*b, text='b', size=15, ha='left', va='top') # ベクトルbラベル
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')
ax.set_ylabel('y')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+')', loc='left')
fig.suptitle('vector a, b', fontsize=20)
ax.set_aspect('equal')
plt.show()

元のベクトル

 ベクトル \mathbf{a}, \mathbf{b}のグラフをaxes.quiver()で描画します。第1・2引数に始点の座標、第3・4引数にベクトルのサイズ(変化量)を指定します。この例では、原点を始点とします。
 配列a, bの前に*を付けてアンパック(展開)して指定しています。

 点 \mathbf{a}, \mathbf{b}を通る直線を計算します。

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

# 傾きを計算
slope = (b[1] - a[1]) / (b[0] - a[0])
print(slope)

# 切片を計算
intercept = a[1] - slope * a[0]
print(intercept)

# x軸の値を作成
x_line = np.linspace(start=x_min, stop=x_max, num=101)

# y軸の値を計算
y_line = slope * x_line + intercept
#y_line = (x_line - a[0]) * (b[1] - a[1]) / (b[0] - a[0]) + a[1]
0.4
2.2

 2点を通る直線の傾き(変化量)は \alpha = \frac{b_2 - a_2}{b_1 - a_1}、切片は \beta = a_2 - \alpha a_1(または \beta = b_2 - \alpha b_1)で計算できます。
 各軸の描画範囲*_min, *_maxを使ってx軸の値を作成して、y軸の値を y = \alpha x + \betaで計算します。

 原点と点 \mathbf{a}, \mathbf{b}、2点を通る直線を描画します。

# 点a,bを通る直線を作図
fig, ax = plt.subplots(figsize=(6, 4), facecolor='white')
ax.quiver(0, 0, *a, color='red', angles='xy', scale_units='xy', scale=1, label='a') # ベクトルa
ax.quiver(0, 0, *b, color='blue', angles='xy', scale_units='xy', scale=1, label='b') # ベクトルb
ax.scatter(0, 0, c='orange', s=50) # 原点
ax.scatter(*a, c='red', s=50) # 点a
ax.scatter(*b, c='blue', s=50) # 点b
ax.plot(x_line, y_line) # 点a,bを通る直線
ax.annotate(xy=[0, 0], text='O', size=15, ha='left', va='top') # 原点ラベル
ax.annotate(xy=a, text='a', size=15, ha='right', va='bottom') # 点aラベル
ax.annotate(xy=b, text='b', size=15, ha='left', va='bottom') # 点bラベル
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')
ax.set_ylabel('y')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+')', loc='left')
fig.suptitle('point a, b', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

2点を通る直線

 原点から \mathbf{a}, \mathbf{b}移動した点が \mathbf{a}, \mathbf{b}です(1.1節・1.2節)。

  (1 - \theta) \mathbf{a} + \theta \mathbf{b}のベクトルを描画します。

# 2Dベクトルの線形結合を作図
fig, ax = plt.subplots(figsize=(6, 4), facecolor='white')
ax.scatter(0, 0, c='orange', s=50) # 原点
ax.scatter(*a, c='red', s=50, label='a') # 点a
ax.scatter(*b, c='blue', s=50, label='b') # 点b
ax.scatter(*(1.0-theta)*a+theta*b, c='purple', s=50, label='c') # 点c
ax.plot(x_line, y_line) # 点a,bを通る直線
ax.quiver(0, 0, *(1.0-theta)*a, color='red', angles='xy', scale_units='xy', scale=1) # ベクトル(1-θ)a
ax.quiver(*(1.0-theta)*a, *theta*b, color='blue', angles='xy', scale_units='xy', scale=1) # ベクトルθb
ax.quiver(0, 0, *(1.0-theta)*a+theta*b, color='purple', angles='xy', scale_units='xy', scale=1) # ベクトルc
ax.annotate(xy=0.5*(1.0-theta)*a, text='$(1 - \\theta) a$', size=15, ha='left', va='bottom') # ベクトル(1-θ)aラベル
ax.annotate(xy=(1.0-theta)*a+0.5*theta*b, text='$\\theta b$', size=15, ha='right', va='bottom') # ベクトルθbラベル
ax.annotate(xy=0.5*((1.0-theta)*a+theta*b), text='$c$', size=15, ha='center', va='top') # ベクトルcラベル
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')
ax.set_ylabel('y')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+'), ' + 
             '$\\theta$='+str(theta), loc='left')
fig.suptitle('$c = (1 - \\theta) a + \\theta b$', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルのアフィン結合

 ベクトル \mathbf{a}, \mathbf{b}とスカラーの積和(線形結合)を (1 - \theta) \mathbf{a} + \theta \mathbf{b}とします。

 係数の値を変化させたアニメーションを作成します。

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

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

# ベクトルを指定
a = np.array([-3.0, 1.0])
b = np.array([2.0, 3.0])

# 係数として利用する値を指定
theta_vals = np.linspace(start=-2.0, stop=3.0, num=frame_num)

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

# 点a,bを通る直線を計算
slope = (b[1] - a[1]) / (b[0] - a[0])
intercept = a[1] - a[0] * slope
x_line = np.linspace(start=x_min, stop=x_max, num=101)
y_line = slope * x_line + intercept

# 作図用のオブジェクトを初期化
fig, ax = plt.subplots(figsize=(8, 4), facecolor='white')
fig.suptitle('$c = (1 - \\theta) a + \\theta b$', fontsize=20)

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目の係数を作成
    theta = theta_vals[i]
    
    # 2Dベクトルの線形結合を作図
    ax.scatter(0, 0, c='orange', s=50) # 原点
    ax.scatter(*a, c='red', s=50, label='a') # 点a
    ax.scatter(*b, c='blue', s=50, label='b') # 点b
    ax.scatter(*(1.0-theta)*a+theta*b, c='purple', s=50, label='c') # 点c
    ax.plot(x_line, y_line) # 点a,bを通る直線
    ax.quiver(0, 0, *(1.0-theta)*a, color='red', angles='xy', scale_units='xy', scale=1) # ベクトル(1-θ)a
    ax.quiver(*(1.0-theta)*a, *theta*b, color='blue', angles='xy', scale_units='xy', scale=1) # ベクトルθb
    ax.quiver(0, 0, *(1.0-theta)*a+theta*b, color='purple', angles='xy', scale_units='xy', scale=1) # ベクトルc
    ax.annotate(xy=0.5*(1.0-theta)*a, text='$(1 - \\theta) a$', size=10, ha='left', va='bottom') # ベクトル(1-θ)aラベル
    ax.annotate(xy=(1.0-theta)*a+0.5*theta*b, text='$\\theta b$', size=10, ha='right', va='bottom') # ベクトルθbラベル
    ax.annotate(xy=0.5*((1.0-theta)*a+theta*b), text='$c$', size=10, ha='center', va='top') # ベクトルcラベル
    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')
    ax.set_ylabel('y')
    ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
                 'b=('+', '.join(map(str, b))+'), ' + 
                 '$\\theta$='+str(theta.round(2)), loc='left')
    ax.set_aspect('equal')
    ax.legend()

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

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

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

2次元空間上のアフィン結合


3次元の場合

 続いて、3次元空間上でのアフィン結合を可視化します。

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

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

# 係数を指定
theta = -0.4

   \mathbf{a} = (a_1, a_2, a_3)^{\top}, \mathbf{b} = (b_1, b_2, b_3)^{\top}a, b、係数 \thetathetaとして値を指定します。

 3次元空間上に2つのベクトル \mathbf{a}, \mathbf{b}を描画します。

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

# (原点からの)3Dベクトルを作図
fig, ax = plt.subplots(subplot_kw={'projection': '3d'}, 
                       figsize=(7, 7), facecolor='white')
ax.quiver(0, 0, 0, *a, color='red', arrow_length_ratio=0.1) # ベクトルa
ax.quiver(0, 0, 0, *b, color='blue', arrow_length_ratio=0.1) # ベクトルb
ax.text(*0.5*a, s='a', size=15, ha='center', va='top') # ベクトルaラベル
ax.text(*0.5*b, s='b', size=15, ha='left', va='top') # ベクトルbラベル
ax.quiver([0, a[0], b[0]], [0, a[1], b[1]], [z_min, z_min, z_min], 
          [0, 0, 0], [0, 0, 0], [-z_min, a[2]-z_min, b[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')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+')', loc='left')
fig.suptitle('vector a, b', fontsize=20)
ax.set_aspect('equal')
plt.show()

元のベクトル

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

 点 \mathbf{a}, \mathbf{b}を通る直線を計算します。

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

# 傾きを計算
slope_xy = (b[1] - a[1]) / (b[0] - a[0])
slope_xz = (b[2] - a[2]) / (b[0] - a[0])
print(slope_xy, slope_xz)

# 切片を計算
intercept_xy = a[1] - slope_xy * a[0]
intercept_xz = a[2] - slope_xz * a[0]
print(intercept_xy, intercept_xz)

# x軸の値を作成
x_line = np.linspace(start=x_min, stop=x_max, num=101)

# y・z軸の値を計算
y_line = slope_xy * x_line + intercept_xy
z_line = slope_xz * x_line + intercept_xz
#d = b - a
#y_line = (x_line - a[0]) * d[1] / d[0] + a[1]
#z_line = (x_line - a[0]) * d[2] / d[0] + a[2]
0.4 0.4
2.2 3.2

 x軸とy軸に注目すると、2点を通る直線の傾き(変化量)は \alpha_{(xy)} = \frac{b_2 - a_2}{b_1 - a_1}、切片は \beta_{(xy)} = a_2 - \alpha_{(xy)} a_1(または \beta_{(xy)} = b_2 - \alpha_{(xy)} b_1)で計算できます。同様にx軸とz軸に注目すると、傾きは \alpha_{(xz)} = \frac{b_3 - a_3}{b_1 - a_1}、切片は \beta_{(xz)} = a_3 - \alpha_{(xz)} a_1(または \beta_{(xz)} = b_3 - \alpha_{(xz)} b_1)で計算できます。
 各軸の描画範囲*_min, *_maxを使ってx軸の値を作成して、y軸の値を y = \alpha_{(xy)} x + \beta_{(xy)}、z軸の値を z = \alpha_{(xz)} x + \beta_{(xz)}で計算します。

 原点と点 \mathbf{a}, \mathbf{b}、2点を通る直線を描画します。

# 点a,bを通る直線を作図
fig, ax = plt.subplots(subplot_kw={'projection': '3d'}, 
                       figsize=(7, 7), facecolor='white')
ax.scatter(0, 0, c='orange', s=50, zorder=0) # 原点
ax.scatter(*a, c='red', s=50) # 点a
ax.scatter(*b, c='blue', s=50) # 点b
ax.plot(x_line, y_line, z_line) # 点a,bを通る直線
ax.quiver(0, 0, 0, *a, color='red', arrow_length_ratio=0.1, label='a', zorder=50) # ベクトルa
ax.quiver(0, 0, 0, *b, color='blue', arrow_length_ratio=0.1, label='b', zorder=50) # ベクトルb
ax.text(0, 0, 0, s='O', size=15, ha='left', va='top', zorder=20) # 原点ラベル
ax.text(*a, s='a', size=15, ha='right', va='bottom', zorder=20) # 点aラベル
ax.text(*b, s='b', size=15, ha='left', va='bottom', zorder=20) # 点bラベル
ax.quiver([0, a[0], b[0]], [0, a[1], b[1]], [z_min, z_min, z_min], 
          [0, 0, 0], [0, 0, 0], [-z_min, a[2]-z_min, b[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')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+')', loc='left')
fig.suptitle('point a, b', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

2点を通る直線

 3次元でも同様なのを確認できます。

  (1 - \theta) \mathbf{a} + \theta \mathbf{b}のベクトルを描画します。

# 3Dベクトルの線形結合を作図
fig, ax = plt.subplots(subplot_kw={'projection': '3d'}, 
                       figsize=(7, 7), facecolor='white')
ax.scatter(0, 0, c='orange', s=50, zorder=0) # 原点
ax.scatter(*a, c='red', s=50, label='a') # 点a
ax.scatter(*b, c='blue', s=50, label='b') # 点b
ax.scatter(*(1.0-theta)*a+theta*b, c='purple', s=50, label='c') # 点c
ax.plot(x_line, y_line, z_line) # 点a,bを通る直線
ax.quiver(0, 0, 0, *(1.0-theta)*a, color='red', arrow_length_ratio=0.1, zorder=50) # ベクトル(1-θ)a
ax.quiver(*(1.0-theta)*a, *theta*b, color='blue', arrow_length_ratio=0.1, zorder=50) # ベクトルθb
ax.quiver(0, 0, 0, *(1.0-theta)*a+theta*b, color='purple', arrow_length_ratio=0.1, zorder=50) # ベクトル
ax.text(0, 0, 0, s='O', size=15, ha='left', va='top', zorder=20) # 原点ラベル
ax.text(*0.5*(1.0-theta)*a, s='$(1 - \\theta) a$', size=15, ha='left', va='bottom', zorder=20) # ベクトル(1-θ)aラベル
ax.text(*(1.0-theta)*a+0.5*theta*b, s='$\\theta b$', size=15, ha='right', va='bottom', zorder=20) # ベクトルθbラベル
ax.text(*0.5*((1.0-theta)*a+theta*b), s='c', size=15, ha='center', va='top', zorder=20) # ベクトルcラベル
ax.quiver([0, a[0], b[0]], [0, a[1], b[1]], [z_min, z_min, z_min], 
          [0, 0, 0], [0, 0, 0], [-z_min, a[2]-z_min, b[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')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
             'b=('+', '.join(map(str, b))+'), ' + 
                 '$\\theta$='+str(theta), loc='left')
fig.suptitle('$c = (1 - \\theta) a + \\theta b$', fontsize=20)
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルのアフィン結合

 アフィン結合したベクトルを \mathbf{c} = (1 - \theta) \mathbf{a} + \theta \mathbf{b}とします。

 係数の値を変化させたアニメーションを作成します。

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

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

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

# 係数として利用する値を指定
theta_vals = np.linspace(start=-2.0, stop=3.0, num=frame_num)

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

# 点a,bを通る直線を計算
d = b - a
x_line = np.linspace(start=x_min, stop=x_max, num=101)
y_line = (x_line - a[0]) * d[1] / d[0] + a[1]
z_line = (x_line - a[0]) * d[2] / d[0] + a[2]

# 作図用のオブジェクトを初期化
fig, ax = plt.subplots(subplot_kw={'projection': '3d'}, 
                       figsize=(7, 7), facecolor='white')
fig.suptitle('$c = (1 - \\theta) a + \\theta b$', fontsize=20)

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目の係数を作成
    theta = theta_vals[i]
    
    # 3Dベクトルの線形結合を作図
    ax.scatter(0, 0, c='orange', s=50, zorder=0) # 原点
    ax.scatter(*a, c='red', s=50, label='a') # 点a
    ax.scatter(*b, c='blue', s=50, label='b') # 点b
    ax.scatter(*(1.0-theta)*a+theta*b, c='purple', s=50, label='c') # 点c
    ax.plot(x_line, y_line, z_line) # 点a,bを通る直線
    ax.quiver(0, 0, 0, *(1.0-theta)*a, arrow_length_ratio=0.1, color='red', zorder=50) # ベクトル(1-θ)a
    ax.quiver(*(1.0-theta)*a, *theta*b, arrow_length_ratio=0.1, color='blue', zorder=50) # ベクトルθb
    ax.quiver(0, 0, 0, *(1.0-theta)*a+theta*b, arrow_length_ratio=0.1, color='purple', zorder=50) # ベクトル
    ax.text(*0.5*(1.0-theta)*a, s='$(1 - \\theta) a$', size=10, ha='center', va='center', zorder=20) # ベクトル(1-θ)aラベル
    ax.text(*(1.0-theta)*a+0.5*theta*b, s='$\\theta b$', size=10, ha='left', va='top', zorder=20) # ベクトルθbラベル
    ax.text(*0.5*((1.0-theta)*a+theta*b), s='c', size=10, ha='right', va='top', zorder=20) # ベクトルcラベル
    ax.quiver([0.0, (1.0-theta)*a[0], (1.0-theta)*a[0]+theta*b[0]], 
              [0.0, (1.0-theta)*a[1], (1.0-theta)*a[1]+theta*b[1]], 
              [z_min, z_min, z_min], 
              [0, 0, 0], 
              [0, 0, 0], 
              [-z_min, (1.0-theta)*a[2]-z_min, (1.0-theta)*a[2]+theta*b[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')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    ax.set_title('a=('+', '.join(map(str, a))+'), ' + 
                 'b=('+', '.join(map(str, b))+'), ' + 
                 '$\\theta$='+str(theta.round(2)), loc='left')
    ax.set_aspect('equal')
    ax.legend()

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

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

3次元空間上のベクトルのアフィン結合


 この記事では、アフィン結合を可視化しました。次の記事では、内積の計算を確認します。

参考書籍

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

おわりに

 3次元空間上の2点を通る直線を引くのに一苦労しました。調べても謎の等式が出てくるばかりで、でそれは何なんだ?となってました。ダメ元でyとxの式とzとxの式に整理してみたら上手く直線を描けて、だったらそう言って!となりました。変形後の式を見たら2次元のときの傾きと切片の計算式と同じ形になってますね、あの等式を見れば自明なんでしょうか。

 そんなこんなで、アフィン結合の結果が直線上を通ることを可視化できました。見ていて気持ちいアニメになって満足です。でもなぜ直線になるのかは分かってません。

【次の内容】

www.anarchive-beta.com