はじめに
「文系だから数学は苦手」──そんな思い込み、コンプレックスはありませんか。
僕自身、文系出身でありながら ROS や ML(機械学習)に興味を持ちつつも、数学の基礎でつまずくことが多くありました。
※アマチュア無線技士3級ですが、余弦定理ですら目がくらみます。
「基礎数学1 線型代数入門」齋藤正彦著(東京大学出版)なんて、手に取るだけで背筋が伸びるレベル・・・
そんな中、古本で手に入れた『インターフェース 2025年1月号』(CQ出版社)の特集「数学&図解でディープラーニング」に出会い、ベクトルや行列をもう一度ちゃんとやってみたいという気持ちが再燃しました。
これがまた、おもしろいしわかりやすい。この雑誌が 800 円(税・送料込み)なんて本当にありがたいことです。
ところが、付録の MATLAB ライセンスはすでに期限切れ。
図とコードを読むだけではイメージが掴みにくく、実際に動かしてみたい気持ちがどんどん高まるばかり。
そこで、ちょっと寄り道。Copilot にお願いして、まずは「MATLAB の図20(ポリゴンと法線ベクトル)」を Python+Plotly で再現してもらいました。
上記が今回作ったものです。
ぱっと見ではどの面がどうなっているのか、やっぱりわかりませんよね。
この図、「実際に動かして見られたらもっと理解できそうだけど・・・」って感じではありませんか。
Jupyter Notebook 上でマウス操作できる 3D モデルとして、視覚的にも直感的にも理解が深まるものが、あっという間に完成しました。
この記事では、その過程と成果を簡単にご紹介します。
やりたいこと
「インターフェース」2025年1月号(CQ出版社)
特集 数学&図解でディープラーニング
第2章 ベクトルと行列(新井正敏 氏)
2-3 ベクトルの適用とご利益
その②・・・ベクトルと3Dグラフィックス
●ポリゴンでは平面の裏表が重要
図20 外積を使えばポリゴンの面の向きがわかる(p.67)
この図を実際に動かして、
- どこにどんな面があるのか
- 法線がどちらを向いているのか
- 裏表の違いがどう見えるのか
を確認してみたかった、というのが今回のモチベーションです。
紙面の図だけだと、どうしても立体感がつかみにくいんですよね。
また、以下は紙面にも掲載されている「リスト11 図20の4面体を表示する MATLAB コード(polygon001.m)」です。特集サポートページから誰でもダウンロードできます。
MATLABコード(プログラム全文を引用)
close all;
clear all;
% ポリゴンの頂点を定義
Q = [1, 1, 1];
A = [2, 1.2, 1];
B = [1.5, 1.5, 1.2];
C = [1.8, 0.8, 1.5];
% ポリゴンをプロット
figure;
fill3([Q(1), B(1), A(1)], [Q(2), B(2), A(2)], [Q(3), B(3), A(3)], 'g', 'FaceAlpha', 0.5, 'DisplayName', 'Poly1');
hold on;
fill3([Q(1), A(1), C(1)], [Q(2), A(2), C(2)], [Q(3), A(3), C(3)], 'r', 'FaceAlpha', 0.1, 'DisplayName', 'Poly2');
fill3([Q(1), B(1), C(1)], [Q(2), B(2), C(2)], [Q(3), B(3), C(3)], 'b', 'FaceAlpha', 0.1, 'DisplayName', 'Poly3');
fill3([A(1), B(1), C(1)], [A(2), B(2), C(2)], [A(3), B(3), C(3)], 'y', 'FaceAlpha', 0.5, 'DisplayName', 'Poly4');
% 各ポリゴンの頂点の法線ベクトルを算出
normal1 = cross(B - Q, A - Q);
normal2 = cross(A - Q, C - Q);
normal3 = cross(C - Q, B - Q);
normal4 = cross(B - A, C - A);
% 各ポリゴンの中心座標を算出
center1 = mean([Q; A; B]);
center2 = mean([Q; A; C]);
center3 = mean([Q; B; C]);
center4 = mean([A; B; C]);
% 法線ベクトルを表示
quiver3(center1(1), center1(2), center1(3), normal1(1), normal1(2), normal1(3), 'g', 'LineWidth', 2, 'MaxHeadSize', 0.5, 'DisplayName', 'Norm1');
quiver3(center2(1), center2(2), center2(3), normal2(1), normal2(2), normal2(3), 'r', 'LineWidth', 2, 'MaxHeadSize', 0.5, 'DisplayName', 'Norm2');
quiver3(center3(1), center3(2), center3(3), normal3(1), normal3(2), normal3(3), 'b', 'LineWidth', 2, 'MaxHeadSize', 0.5, 'DisplayName', 'Norm3');
quiver3(center4(1), center4(2), center4(3), normal4(1), normal4(2), normal4(3), 'y', 'LineWidth', 2, 'MaxHeadSize', 0.5, 'DisplayName', 'Norm4');
% 各点をプロット
scatter3(Q(1), Q(2), Q(3), 100, 'k', 'filled', 'DisplayName', 'Q');
scatter3(A(1), A(2), A(3), 100, 'r', 'filled', 'DisplayName', 'A');
scatter3(B(1), B(2), B(3), 100, 'b', 'filled', 'DisplayName', 'B');
scatter3(C(1), C(2), C(3), 100, 'g', 'filled', 'DisplayName', 'C');
% 軸ラベル表示
xlabel('$x$', 'Interpreter', 'latex', 'FontSize', 14);
ylabel('$y$', 'Interpreter', 'latex', 'FontSize', 14);
zlabel('$z$', 'Interpreter', 'latex', 'FontSize', 14);
legend;
grid on;
Copilotへのプロンプト
添付のMATLAB用のコードを以下の要件でJupyter Notebookで動く3D可視化コードに書き換えてください。
立体図はマウス操作で回転・ズーム・パンできること。
Plotlyをつかう
ポリゴンの頂点の記号(Q,A,B,C)を表示させる。
頂点ラベルの色(アルファベット文字は黒、頂点ラベルはQ黒、A橙、B青、C緑)
ポリゴンの各面にそれぞれ配色する。
ポリゴンの色(poly1緑、poly2ピンク、poly3紫、poly4黄)
ポリゴンの面ごとの法線ベクトルをそれぞれ描画する。
法線の色(norm1緑、norm2橙、norm3青、norm4黄)
右上に上記の色分けの凡例は英語表記でシンプルに表示する。
matplotlib のフォント設定をいじらない
最初はMatplotlibでのアニメーションを試しましたが、ポリゴンの反対側からもじっくり見たい、やっぱりマウスでグリグリ動かしたい!ということでPlotlyに切り替えました。
そして実際にCopilotが作ったコード
コードをみる
import numpy as np
import plotly.graph_objects as go
# -----------------------------
# 1. Define vertices (Q, A, B, C)
# -----------------------------
Q = np.array([1.0, 1.0, 1.0])
A = np.array([2.0, 1.2, 1.0])
B = np.array([1.5, 1.5, 1.2])
C = np.array([1.8, 0.8, 1.5])
# Faces (each is a triangle)
# Poly1: Q-B-A (green)
# Poly2: Q-A-C (pink)
# Poly3: Q-B-C (purple)
# Poly4: A-B-C (yellow)
faces = {
"Poly1": (Q, B, A),
"Poly2": (Q, A, C),
"Poly3": (Q, B, C),
"Poly4": (A, B, C),
}
# -----------------------------
# 2. Compute face centers and normals
# -----------------------------
def face_center(p1, p2, p3):
return (p1 + p2 + p3) / 3.0
def face_normal(p1, p2, p3):
# Cross product of two edges
n = np.cross(p2 - p1, p3 - p1)
# Normalize
norm = np.linalg.norm(n)
if norm == 0:
return n
return n / norm
centers = {}
normals = {}
centers["Poly1"] = face_center(Q, B, A)
centers["Poly2"] = face_center(Q, A, C)
centers["Poly3"] = face_center(Q, B, C)
centers["Poly4"] = face_center(A, B, C)
normals["Poly1"] = face_normal(Q, B, A)
normals["Poly2"] = face_normal(Q, A, C)
normals["Poly3"] = face_normal(Q, B, C)
normals["Poly4"] = face_normal(A, B, C)
# Scale normals for visualization
normal_scale = 0.5
# -----------------------------
# 3. Polygon faces (Mesh3d, one trace per face)
# -----------------------------
poly_colors = {
"Poly1": "green", # poly1 緑
"Poly2": "pink", # poly2 ピンク
"Poly3": "purple", # poly3 紫
"Poly4": "yellow", # poly4 黄
}
poly_traces = []
for name, (p1, p2, p3) in faces.items():
xs = [p1[0], p2[0], p3[0]]
ys = [p1[1], p2[1], p3[1]]
zs = [p1[2], p2[2], p3[2]]
# Single triangle: indices (0,1,2)
poly_traces.append(
go.Mesh3d(
x=xs,
y=ys,
z=zs,
i=[0],
j=[1],
k=[2],
color=poly_colors[name],
opacity=0.5,
name=name,
showlegend=True
)
)
# -----------------------------
# 4. Normals (one trace per face, as cones)
# -----------------------------
normal_colors = {
"Poly1": "green", # norm1 緑
"Poly2": "orange", # norm2 橙
"Poly3": "blue", # norm3 青
"Poly4": "yellow", # norm4 黄
}
normal_traces = []
for name in ["Poly1", "Poly2", "Poly3", "Poly4"]:
c = centers[name]
n = normals[name] * normal_scale
normal_traces.append(
go.Cone(
x=[c[0]],
y=[c[1]],
z=[c[2]],
u=[n[0]],
v=[n[1]],
w=[n[2]],
sizemode="absolute",
sizeref=0.3,
colorscale=[[0, normal_colors[name]], [1, normal_colors[name]]],
showscale=False,
name=name.replace("Poly", "Norm"), # Norm1, Norm2, ...
showlegend=True
)
)
# -----------------------------
# 5. Vertices (Q, A, B, C) with labels
# -----------------------------
# 頂点ラベルの色:文字は黒
# 頂点マーカー色:Q黒、A橙、B青、C緑
vertex_points = np.vstack([Q, A, B, C])
vertex_labels = ["Q", "A", "B", "C"]
vertex_colors = ["black", "orange", "blue", "green"]
vertex_trace = go.Scatter3d(
x=vertex_points[:, 0],
y=vertex_points[:, 1],
z=vertex_points[:, 2],
mode="markers+text",
marker=dict(
size=8,
color=vertex_colors
),
text=vertex_labels,
textposition="top center",
textfont=dict(color="black"),
name="Vertices",
showlegend=True
)
# -----------------------------
# 6. Figure layout (interactive 3D)
# -----------------------------
fig = go.Figure(data=poly_traces + normal_traces + [vertex_trace])
fig.update_layout(
scene=dict(
xaxis=dict(title="x"),
yaxis=dict(title="y"),
zaxis=dict(title="z"),
aspectmode="data"
),
legend=dict(
x=1.0,
y=1.0,
xanchor="right",
yanchor="top",
bgcolor="rgba(255,255,255,0.7)"
),
margin=dict(l=0, r=0, t=30, b=0),
title="Interactive 3D Polygon with Face Normals (Plotly)"
)
fig.show()
正直、感動しました
Plotly の 3D 表示はマウスでグリグリ動かせるので、
「この面ってこうつながってたのか!」
「法線ってこんな方向に伸びてたのか!」
と、紙面では分からなかった部分が一気にクリアになりました。
まとめ
MATLAB は強力そうですが、個人が趣味で使うにはどうしてもハードルが高いですよね。お値段的に。月 20 時間までは無料で体験できるそうですが、ニューロンのモデル化や Simulink をちょっと触ってみたい程度だと、なかなか本腰を入れる気にもなれません。せっかくコードを覚えても、実際に使う機会がなければ宝の持ち腐れになってしまいます。
一方で、GPU をしっかり積んだ mint + CUDA の自作 PC でディープラーニングやローカル LLM を触るなら、やっぱり OSS と Jupyter Notebook の組み合わせが気楽で便利。まして、Copilot がさっと 3D モデルを示して教科書の難解な説明図をスルスルっと解きほぐす様子は、まるで会社の切れ者の後輩が、おじさんに横からさらりと苦もなく知恵を授けてくれるかのようでした。
特に、MATLAB のプログラムをそのまま Copilot に読み込ませて Python に変換できるのは、学習目的に照らして時間効率がよく非常に有効な方法だと感じました。
まさかここまで気楽に「動かして理解する」体験ができるとは思いませんでした。
次は、あの“全微分”あたりで出てくる曲面と平面の接点や法線の描写にも挑戦してみたいところです。
