2022年4月13日水曜日

プログラミングに数学の素養は必要か?

「プログラミングのための数学」という科目を2019年から担当している.高校では数IAしか学んでいませんという学生と,数IIIまでばっちりやりましたという学生が同時に履修しているので,なかなかやりにくい科目ではある.しかし,高校で学ぶべき数学のリメディアル教育を進めるとともに,数IIIまでしっかり学びましたという学生に向けて「大学レベルの数学」も提供するというアクロバティックな講義でもあり,やり甲斐はあると感じている.

プログラミングに使われる数学(再帰の理解)

さて,今日はそのイントロ,第1回めの講義が行われた.そこで「プログラミングに数学は必要か」という話題を入念に紹介した.

例の一つとして「ヒルベルト曲線の描画」を挙げた(以前,本ブログでも取り上げたテーマである).そもそもフラクタルとは何か?から説明する必要があったが,見た目に面白いので学生も興味深く聴いてくれたようである.ヒルベルト曲線に限らず,フラクタル図形の描画は,再帰を用いると簡単に描ける.自己相似形というその性質が,再帰の考え方と合致するからである.

再帰の考えは,慣れないと難しいとされている.プログラミングの試験問題に再帰呼び出しを入れると,正解率がガクンと下がるらしい.ただし,一度理解できれば漸化式をそのまま記述できるので,プログラムは読みやすくなる(はずである).

再帰呼び出しを理解するために数学の素養としてはなにが必要だろうか.まず,関数呼び出しの考え方は,数学における関数の考えと類似の概念である.したがって,関数とは何かを理解する必要があろう.また,高校数学では数列を学び,漸化式と数学的帰納法を学ぶ.その考え方をしっかりと理解していれば,再帰呼び出しはそれをプログラムで表現しているだけであるということに気付くであろう.

プログラミングに使われる数学(行列・線形代数)

ヒルベルト曲線を描画するには,線形代数の理解も不可欠である.平面に線を描画するので,座標の概念を理解していないといけない.また,再帰とアフィン変換を組み合わせて部分的な自己相似形を描画し,それを統合することで高次のフラクタルを描くので,座標変換のアイデアが必須となる.

それを効率的に記述するには行列の理解が不可欠である.例を示そう.次の例は,2次のヒルベルト曲線を描くプログラムである.

#!/usr/bin/env python


from turtle import *


SIZE = 300

penup_flag = True

tm = [

  [ [0.0, -0.5, -0.5], [-0.5, 0.0,  0.5], [0.0, 0.0, 1.0] ],

  [ [0.5,  0.0, -0.5], [ 0.0, 0.5, -0.5], [0.0, 0.0, 1.0] ],

  [ [0.5,  0.0,  0.5], [ 0.0, 0.5, -0.5], [0.0, 0.0, 1.0] ],

  [ [0.0,  0.5,  0.5], [ 0.5, 0.0,  0.5], [0.0, 0.0, 1.0] ]

]

e = [ [ 1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0] ]

p = [[-0.5, 0.5], [-0.5, -0.5], [0.5, -0.5], [0.5, 0.5]]


def affine_transform(m, p):

  return [ m[0][0] * p[0] + m[0][1] * p[1] + m[0][2],

           m[1][0] * p[0] + m[1][1] * p[1] + m[1][2] ]


def mat_mul(m0, m1):

  return [ [m0[0][0]*m1[0][0]+m0[0][1]*m1[1][0]+m0[0][2]*m1[2][0],

            m0[0][0]*m1[0][1]+m0[0][1]*m1[1][1]+m0[0][2]*m1[2][1],

            m0[0][0]*m1[0][2]+m0[0][1]*m1[1][2]+m0[0][2]*m1[2][2]],

           [m0[1][0]*m1[0][0]+m0[1][1]*m1[1][0]+m0[1][2]*m1[2][0],

            m0[1][0]*m1[0][1]+m0[1][1]*m1[1][1]+m0[1][2]*m1[2][1],

            m0[1][0]*m1[0][2]+m0[1][1]*m1[1][2]+m0[1][2]*m1[2][2]],

           [m0[2][0]*m1[0][0]+m0[2][1]*m1[1][0]+m0[2][2]*m1[2][0],

            m0[2][0]*m1[0][1]+m0[2][1]*m1[1][1]+m0[2][2]*m1[2][1],

            m0[2][0]*m1[0][2]+m0[2][1]*m1[1][2]+m0[2][2]*m1[2][2]] ]


def screen_pos(x):

  return (x[0]*SIZE, x[1]*SIZE)


setup(width = 2*SIZE, height = 2*SIZE)

color('red')

width(5)

hideturtle()


penup()

for m in tm:

  p2 = [ affine_transform(mat_mul(e, m), x) for x in p ]

  for x in p2:

    goto(screen_pos(x))

    if penup_flag:

      pendown()

      penup_flag = False


onkey(exit, 'q')

listen()

mainloop()

ここでも行列の考え方は使っているが,この計算はずいぶんと大変な印象を受ける.一方,これをNumPyの行列計算ライブラリを利用することで,次のように実に簡潔に書ける(再帰を使ってn次のヒルベルト曲線を描くようにすると,実は,もっと単純化することができる).

#!/usr/bin/env python


from turtle import *

import numpy as np


SIZE = 300

penup_flag = True

tm = [ np.matrix(x) for x in [

  [ [0.0, -0.5, -0.5], [-0.5, 0.0,  0.5], [0.0, 0.0, 1.0] ],

  [ [0.5,  0.0, -0.5], [ 0.0, 0.5, -0.5], [0.0, 0.0, 1.0] ],

  [ [0.5,  0.0,  0.5], [ 0.0, 0.5, -0.5], [0.0, 0.0, 1.0] ],

  [ [0.0,  0.5,  0.5], [ 0.5, 0.0,  0.5], [0.0, 0.0, 1.0] ] ]

]

e = np.eye(3) # 3次の単位行列

p = [ np.matrix(x).T for x in [

      [-0.5,  0.5, 1.0], [-0.5, -0.5, 1.0],

      [ 0.5, -0.5, 1.0], [ 0.5,  0.5, 1.0] ] ]


def screen_pos(x):

  return (x[0,0]*SIZE, x[1,0]*SIZE)


setup(width = 2*SIZE, height = 2*SIZE)

color('red')

width(5)

hideturtle()


penup()

for m in tm:

  for x in [ e * m * x for x in p ]:

    goto(screen_pos(x))

    if penup_flag:

      pendown()

      penup_flag = False


onkey(exit, 'q')

listen()

mainloop()

しかし,このプログラムを理解するためには,行列の概念を学んでおく必要がある.「np.matrix(x).T」というコードの「.T」が,転置行列を表すという例が端的な例であろう.転置行列とは何か,単純な概念ではあるが,知らないとわからない.

基礎は大切.プログラミングも数学も

プログラミングに数学は必要か?という質問に対する答えは「直接的には必要ない場合も多いようにみえるかもしれない,しかし,数学を学んだほうが,効率的で間違いのないプログラミングを実現できる」と答えておこう.相次ぐシステムトラブルも,数学を忌避してよけいな回り道を通った実装の結果ではないかと穿った見方をしているが,それは意地悪すぎだろうか?

0 件のコメント:

コメントを投稿