ケィオスの時系列解析メモランダム

時系列解析、生体情報解析などをやわらかく語ります

コッホ曲線の物語:数学的創造物からフラクタルのアイコンへ

19世紀後半まで、数学者の多くは「連続な曲線は、ほとんどの点で滑らかに接線が引ける」という幾何学的直感を、半ば当然のものとして受け入れていました。ところが、この直感は19世紀末に大きく揺さぶられます。

 Karl Weierstrass1872年、いたるところ連続でありながら、どの点でも微分できない関数を提示しました。これは、「連続な曲線は局所的には直線のように振る舞うはずだ」という当時の解析学的・幾何学的直感を、理論の内部から否定する決定的な反例でした。この結果により、連続性と微分可能性は本質的に異なる概念であることが明確になり、解析学は直感よりも厳密性を優先する方向へと大きく舵を切ることになります。

Figure: Weierstrass 関数の例。本記事の末尾に示した R スクリプトの実行結果。

 同じ時代背景の中で、Georg Cantor による集合論もまた、数学的世界観に深い衝撃を与えました。とくにカントール集合は、「全体の長さはゼロであるにもかかわらず、無限個の点を含む」という性質をもち、長さ・次元・無限といった概念が、従来のユークリッド幾何では捉えきれないことを示しました。これらの構成は、当時の感覚からすれば極めて奇妙であり、「自然界には現れない純粋に理論的な対象」と見なされることが少なくありませんでした。

 この流れの中で、1903年Teiji Takagi(高木貞治)が発表した高木曲線(Takagi curve)も、同種の問題意識に基づく重要な例として位置づけられます。高木曲線は、三角波をスケールを変えて無限に重ね合わせることで定義される関数であり、いたるところ連続だが、どの点でも微分不可能という性質をもっています。ワイエルシュトラスの例が解析的にやや抽象的であったのに対し、高木曲線は比較的単純な関数の重ね合わせとして表現でき、しかもグラフとして明確なギザギザ構造を示す点で、後世の研究者に強い印象を与えました。

Figure: 高木関数の例。本記事末尾の R スクリプトの実行結果。

 しかし重要なのは、ワイエルシュトラス関数、高木曲線、カントール集合のいずれもが、この時点では「自然の形」を説明するために導入されたものではなかったという事実です。これらはすべて、解析学や集合論における直感の限界を示すための、いわば理論的反例として構成されたものであり、その幾何学的な荒さや自己相似性が、自然界の形態と結びついて理解されるようになるのは、まだ半世紀以上先のことです。こうした対象は当初、pathological(病的な例)と呼ばれ、理論の境界を確認するための反例として扱われていました。自然界を記述するための数学というよりも、解析学の厳密性を保証するための試金石だったのです。

 そのような歴史的背景の中で登場するのが、幾何学的構成によって定義されるフラクタル図形であるコッホ曲線です。本稿では、このコッホ曲線がどのような問題意識から生まれ、時代とともにどのように解釈され、その意味づけが変化してきたのかを、歴史の流れに沿ってたどっていきます。

1. 1904年:Helge von Koch が「図形」として微分不能性を可視化する

 こうした解析学的関心(純粋な数学的興味)に基づく流れの中で登場するのが、Helge von Koch(ヘルゲ・フォン・コッホ)です。コッホはスウェーデンの数学者で、数論(とくに素数分布)でも知られていますが、後世に最も強い印象を残したのは、1904年の論文 “Sur une courbe continue sans tangente, obtenue par une construction géométrique élémentaire” (「初等的な幾何作図によって得られる、接線をもたない連続曲線について」)でした。

Figure: コッホ(Koch)曲線の自己相似性

 コッホの問題意識はきわめて明確です。連続だが、どこにも接線をもたない曲線を、難解な数式ではなく、誰もが目で追える幾何学的作図として与えることでした。直線を三分割し、中央を折り曲げるという単純な操作を無限に繰り返すことで、曲線はどんどん複雑になります。拡大すればするほど新たな折れが現れ、どの点でも「局所的に直線のように見える」瞬間が訪れません。

 フラクタル幾何学の文脈でコッホ曲線をとらえるとき重要なのは、コッホ自身は自然界の形を説明しようとしていたわけではないという点です。彼の関心は、ワイエルシュトラスに代表される解析学の問題を、幾何学的に直感化することにありました。コッホ曲線は、あくまで「解析学的直感の限界」を示す、厳密で美しい反例だったのです。

■ コッホ曲線(Koch curve)の作り方

 フラクタル図形の特徴の一つは、単純な規則を繰り返し適用することで構成される点にあります。コッホ曲線も、この考え方に基づき、次に示す手順に従って段階的な変形が繰り返されます。

最初は長さ1の線分

中央の1/3を60°折り曲げる

同じ長さの辺を追加(線分の長さはすべて1/3)

各線分を同様に変形(1辺の長さは1/9)

さらに変形(1辺の長さは1/27)

これを無限に繰り返すとコッホ曲線の完成です.コッホ曲線は,部分を拡大しても,全体と同じ構造が現れます.

Figure: ちょっとずつ拡大(zooming)してみると

2. 長い沈黙:コッホ曲線は「奇妙な例」として眠り続ける

 1904年以降、コッホ曲線やカントール集合、ペアノ曲線といった対象は、教科書や講義の中で断片的に登場し続けました。しかしその扱いは一貫して、「普通ではない例」「例外的構成」にとどまっていました。それらが自然の形現実世界の不規則性と結びつくことは、ほとんどありませんでした。なぜなら、当時の数学と物理学は、滑らかな関数と微分方程式を基盤とする世界観を共有していたからです。ギザギザした曲線や無限に折れ曲がる境界は、むしろ「理想化の失敗」と見なされがちでした。

 この状況が大きく変わるのは、20世紀後半になってからです。

3. 1967年:Benoît Mandelbrot が世界の見方を変えた

 Benoît Mandelbrot(ブノワ・マンデルブロ)1967年Science 誌に発表した論文 “How Long Is the Coast of Britain?” は、数学史・科学史の文脈でひとつの転換点と見なされています。

 マンデルブロが投げかけた問いは素朴でした。「イギリスの海岸線の長さは、いったい何 km なのか?」

 測る物差しを短くすればするほど、海岸線の長さは増え続け、一定値に収束しません。この現象を、誤差や測定ミスではなく、形そのものの性質として捉え直した点に、彼の独創性があります。ここで導入されたのが、自己相似性分数次元(fractional dimension)という考え方でした。

 この文脈において、コッホ曲線は突然、新しい意味を帯びます。それはもはや「病的な例」ではなく、自然界の荒さを理解するための理想化モデルになったのです。

4. 1975年以降:「フラクタル」という統一的視点の誕生

 1975年、マンデルブロは著書 Les Objets fractals において、fractal(フラクタル)という用語を導入します。語源はラテン語 fractus(壊れた、砕けた)であり、整数次元では捉えきれない幾何学的対象を包括する言葉でした。

 ここで重要なのは、マンデルブロが新しい図形を大量に発明したわけではないという点です。彼が行ったのは、ワイエルシュトラスの関数、カントール集合、コッホ曲線といった既存の数学的構成物を、「自己相似」「分数次元」という共通言語で再編成したことでした。

 1982年の The Fractal Geometry of Nature によって、この視点は物理学、地学、生理学、経済学にまで広がります。コッホ曲線は、ここで初めて「自然の形を考えるための原型」として、正面から光を当てられることになりました。

5. おわりに:コッホ曲線は「時代を二度生きた」

 歴史を振り返ると、コッホ曲線はきわめて特異な運命をたどっています。1904年には、解析学の直感を打ち破るための厳密な反例として生まれ、1960年代以降には、自然界の不規則性を理解するための象徴的モデルとして再発見されました。この二重の意味を理解することは、フラクタルを単なる「面白い図形」として消費しないために重要です。コッホ曲線は、数学的厳密性と自然観の変革が、別々の時代に、別々の理由で交差した地点に存在しているのです。
 それこそが、コッホ曲線が今なお語られ続ける理由だと言えるでしょう。

[付録] R スクリプト

Weierstrass 関数を描いてみる

以下のスクリプトは、Weierstrass 関数を描画します。Weierstrass 関数は単純な数式で定義されているにもかかわらず、そのグラフは、どのスケールで見ても非常に不規則でギザギザした形状を示します。

ここで用いている Weierstrass 関数は、次式で定義されます。

\displaystyle{
W(x) = \sum_{n=0}^{N} a^{n}\cos\!\left(b^{n}\pi x\right),
}

ここで、

  • \displaystyle{0 \lt a \lt 1} は振幅がどの程度の速さで減衰するかを制御します。
  • \displaystyle{b \gt 1} は整数(通常は奇数を選びます)であり、振動がどれだけ急速に細かくなるかを制御します。
  • \displaystyle{N} は有限和における項数です。

典型的に \displaystyle{ab \gt 1} を満たすパラメータを選ぶと、この関数はいたるところ連続でありながら、どの点にも接線をもたないことが知られています。

スクリプトの内容
  1. weierstrass() 関数

    • x の値のベクトルと、パラメタ a, b, N を入力として受け取ります。
    • 周波数が b^ n で増加し、振幅が a^ n で減少する余弦波の有限和を計算します。
    • 計算効率を高めるために、行列演算を用いたベクトル化計算を行っています。
  2. メインの描画部分

    • 区間 [0,1] 上に一様な格子点を生成します。
    • その格子点上で Weierstrass 関数を評価します。
    • W(x) を連続な曲線として描画します。
  3. オプションのズーム(コメントアウト)

    • より狭い区間(例:[0.30, 0.35])にズームすることで、 小さなスケールでも曲線が粗いままであることを視覚的に確認できます。
スクリプトの使い方・変更方法
  • N を大きくすると、より細かな構造が現れます(ただし計算時間は増加します)。
  • a を変更すると、曲線全体の粗さを調整できます。
  • b を変更すると、新しい振動が現れる速さを調整できます。
  • x の範囲を変更すれば、任意の有限区間で関数を描画できます。
############################################################
# Weierstrass function plot
# W(x) = sum_{n=0}^N a^n cos(b^n * pi * x)
############################################################

weierstrass <- function(x, a = 0.5, b = 7, N = 30) {
  if (!is.numeric(x)) stop("x must be numeric.")
  if (!(a > 0 && a < 1)) stop("Require 0 < a < 1.")
  if (!(b > 1 && abs(b - round(b)) < .Machine$double.eps^0.5)) stop("b must be an integer > 1.")
  if (b %% 2 == 0) warning("b is usually chosen as an odd integer (e.g., 3,5,7,...)")
  if (N < 0 || abs(N - round(N)) > 0) stop("N must be a nonnegative integer.")

  # Vectorized sum over n
  n <- 0:N
  # outer(x, n): matrix with entries x_i * b^n
  # then cos(pi * x_i * b^n), scaled by a^n, summed over n
  Wx <- cos(pi * outer(x, b^n))
  drop(Wx %*% (a^n))
}

############################################################
# Example: draw the function
############################################################

# Parameters (classic-looking choices)
a <- 0.5
b <- 7
N <- 25          # increase for more fine detail (but heavier)

par(mfrow=c(1,1))
# Grid on [0,1]
m <- 4000
x <- seq(0, 1, length.out = m)
y <- weierstrass(x, a = a, b = b, N = N)

# Plot
plot(x, y, type = "l", lwd = 1.5, col = 2,
     xlab = "x", ylab = "W(x)",
     main = sprintf("Weierstrass function: a=%.3f, b=%d, N=%d", a, b, N))
############################################################
# Optional: show "roughness" by zooming into an interval
############################################################
# x2 <- seq(0.30, 0.35, length.out = m)
# y2 <- weierstrass(x2, a = a, b = b, N = N)
#
# plot(x2, y2, type = "l", lwd = 1.5, col = 2,
#     xlab = "x", ylab = "W(x)",
#     main = sprintf("Zoom: [0.30, 0.35], a=%.3f, b=%d, N=%d", a, b, N))

Takagi 関数を描いてみる

以下のスクリプトは、Takagi 関数高木曲線とも呼ばれます)を描画します。これは、次の性質をもつ古典的な関数の例です。

  • いたるところ連続であるが、
  • どの点でも微分不可能である。

Weierstrass 関数とは異なり、Takagi 関数は非常に単純な「テント型」の形状を、スケールを変えながら繰り返し重ね合わせることで構成されます。その結果、どれだけ拡大しても、明確なジグザグ状(のこぎり波状)の粗さが現れます。

ここで用いている Takagi 関数は、次式で定義されます。

\displaystyle{
T(x)=\sum_{n=0}^{N}\frac{1}{2^{n}}\,\phi\!\left(2^{n}x\right),
}

ここで \phi(u) は、u から最も近い整数までの距離を表します。

\displaystyle{
\phi(u)=\min_{k\in\mathbb{Z}}|u-k|.
}

この定義において、

  • \displaystyle{N} は有限和の項数です。
  • \displaystyle{2^ {n}} の因子により、n が大きくなるにつれて振動はより細かくなります。
  • \displaystyle{1/2^ {n}} の重み付けにより、n が大きくなるにつれて各スケールの寄与は小さくなります。

極限 N\to\infty において、Takagi 関数はいたるところ連続でありながら、どの点にも接線をもちません

スクリプトの内容
  1. phi_nearest_int() 関数

    • \phi(u)、すなわち u から最も近い整数までの距離を計算します。
    • |u-\mathrm{round}(u)| として実装されており、高速かつ実用的な計算方法です。
  2. takagi() 関数

    • x の値のベクトルとパラメタ N を入力として受け取ります。
    • 有限和 \sum _ {n=0}^ {N} 2^ {-n}\phi(2^ {n}x) を計算します。
    • 計算効率のため、行列演算(outer と行列積)を用いています。
  3. メインの描画部分

    • 区間 [0,1] 上に一様な格子点を生成します。
    • その格子点上で Takagi 関数を評価します。
    • T(x) を連続な曲線として描画します。
  4. オプションのズーム(コメントアウト)

    • より狭い区間(例:[0.30, 0.35])にズームすることで、 小さなスケールでもギザギザした構造が残ることを確認できます。
スクリプトの使い方・変更方法
  • N を大きくすると、より細かなスケールの構造が現れます(計算時間は増加します)。
  • x の範囲を変更すれば、任意の有限区間で関数を描画できます(標準的には [0,1] を用います)。
  • ズーム区間を変更することで、異なる位置における自己相似的な粗さを強調できます。
############################################################
# Takagi function plot
# T(x) = sum_{n=0}^N (1/2^n) * phi(2^n x),
# where phi(u) = distance from u to the nearest integer
############################################################

# Distance to nearest integer: phi(u) = min_{k in Z} |u - k|
phi_nearest_int <- function(u) {
  # round(u) gives a nearest integer (ties go to even; OK here)
  abs(u - round(u))
}

takagi <- function(x, N = 20) {
  if (!is.numeric(x)) stop("x must be numeric.")
  if (any(is.na(x))) stop("x contains NA.")
  if (N < 0 || abs(N - round(N)) > 0) stop("N must be a nonnegative integer.")

  n <- 0:N
  # Matrix whose (i,n) element is phi(2^n * x_i)
  Phi <- phi_nearest_int(outer(x, 2^n))
  # Weighted sum over n with weights 1/2^n
  drop(Phi %*% (1 / (2^n)))
}

############################################################
# Example: draw the function
############################################################

# Parameter (increase N for finer detail, but heavier)
N <- 20

par(mfrow = c(1,1))

# Grid on [0,1]
m <- 4000
x <- seq(0, 1, length.out = m)
y <- takagi(x, N = N)

# Plot
plot(x, y, type = "l", lwd = 1.5, col = 2,
     xlab = "x", ylab = "T(x)",
     main = sprintf("Takagi function (finite sum): N=%d", N))

############################################################
# Optional: show "roughness" by zooming into an interval
############################################################
# x2 <- seq(0.30, 0.35, length.out = m)
# y2 <- takagi(x2, N = N)
#
# plot(x2, y2, type = "l", lwd = 1.5, col = 2,
#      xlab = "x", ylab = "T(x)",
#      main = sprintf("Zoom: [0.30, 0.35], N=%d", N))

Koch 曲線を描いてみる

以下のスクリプトは、Koch 曲線を描画します。Koch 曲線は、単純な幾何学的規則によって構成される、代表的なフラクタル曲線です。Weierstrass 関数や Takagi 関数とは異なり、Koch 曲線は数式ではなく、幾何学的反復操作によって定義されます。その粗さは、曲線の形そのものとして直接観察できます。

Koch 曲線は、次の手順で構成されます。

  1. 直線の線分から開始する。
  2. 線分を 3 等分する。
  3. 中央の 1/3 を、正三角形の「突起」を形成する 2 本の線分で置き換える。
  4. すべての線分に対して同じ操作を適用し、この過程を無限に繰り返す。

反復回数が増えるにつれて、曲線はますますギザギザになります。どれだけ拡大しても、新しい角が常に現れます。

スクリプトの内容
  1. koch_refine() 関数

    • (x _ i, y _ i) によって定義された折れ線の座標を入力として受け取ります。

    • 各線分を、Koch の作図規則に従って 4 本の新しい線分に置き換えます。

      • 最初の 1/3
      • 正三角形の上り辺
      • 正三角形の下り辺
      • 最後の 1/3
    • 60^ \circ の固定回転を用いて、正三角形の頂点を生成します。

    • 細分化された点の集合を返します。

  2. koch_curve() 関数

    • 初期形状を設定します。

      • 開いた Koch 曲線の場合は 1 本の線分
      • Koch 雪片の場合は正三角形(オプション)
    • 指定した回数だけ koch_refine() を繰り返し適用します。

    • 得られた折れ線の座標を返します。

  3. メインの描画部分

    • 指定した反復回数で koch_curve() を呼び出します。
    • 得られた点列を連続な線として描画します。
    • asp = 1 を指定することで、幾何学的比率を正しく保ちます。
  4. オプションのズーム(コメントアウト)

    • xlimylim を制限することで、局所領域にズームできます。
    • 小さなスケールでも曲線が滑らかにならないことを視覚的に確認できます。
スクリプトの使い方・変更方法
  • iter を増やすと、より細かな幾何構造が現れます (点の数はおおよそ 4^ {\text{iter}} に比例して急増します)。
  • closed = TRUE に設定すると、開いた曲線ではなく Koch 雪片を描画します。
  • xlimylim を調整することで、局所構造や自己相似性を詳しく観察できます。
  • 初期の線分や三角形を変更することで、異なる出発形状を試すことも可能です。
############################################################
# Koch curve plot
# - Start from a line segment
# - Repeatedly replace each segment by 4 segments:
#   divide into thirds and build an outward equilateral "bump"
############################################################

koch_refine <- function(x, y) {
  n <- length(x)
  stopifnot(n >= 2, length(y) == n)

  # Allocate: each segment -> 4 segments => points become 4*(n-1)+1
  x2 <- numeric(4*(n - 1) + 1)
  y2 <- numeric(4*(n - 1) + 1)

  cos60 <- 0.5
  sin60 <- sqrt(3) / 2

  k <- 1
  x2[k] <- x[1]; y2[k] <- y[1]

  for (i in 1:(n - 1)) {
    dx <- x[i + 1] - x[i]
    dy <- y[i + 1] - y[i]

    # Points that divide the segment into thirds
    xa <- x[i] + dx / 3
    ya <- y[i] + dy / 3

    xb <- x[i] + 2 * dx / 3
    yb <- y[i] + 2 * dy / 3

    # Apex point: rotate (dx/3, dy/3) by +60 degrees around (xa, ya)
    rx <- (dx / 3) * cos60 - (dy / 3) * sin60
    ry <- (dx / 3) * sin60 + (dy / 3) * cos60

    xc <- xa + rx
    yc <- ya + ry

    # Append 4 new segments' vertices (excluding the first point already stored)
    x2[k + 1] <- xa; y2[k + 1] <- ya
    x2[k + 2] <- xc; y2[k + 2] <- yc
    x2[k + 3] <- xb; y2[k + 3] <- yb
    x2[k + 4] <- x[i + 1]; y2[k + 4] <- y[i + 1]

    k <- k + 4
  }

  list(x = x2, y = y2)
}

koch_curve <- function(iter = 5, closed = FALSE) {
  if (iter < 0 || abs(iter - round(iter)) > 0) stop("iter must be a nonnegative integer.")

  if (!closed) {
    # Single Koch curve (one segment)
    x <- c(0, 1)
    y <- c(0, 0)
    for (j in 1:iter) {
      tmp <- koch_refine(x, y)
      x <- tmp$x; y <- tmp$y
    }
    return(list(x = x, y = y))
  } else {
    # Koch snowflake (start from an equilateral triangle)
    x <- c(0, 1, 0.5, 0)
    y <- c(0, 0, sqrt(3)/2, 0)
    for (j in 1:iter) {
      tmp <- koch_refine(x, y)
      x <- tmp$x; y <- tmp$y
    }
    return(list(x = x, y = y))
  }
}

############################################################
# Example: draw the curve
############################################################

iter <- 7          # increase for finer detail (points grow as 4^iter)
closed <- FALSE    # TRUE => Koch snowflake

dat <- koch_curve(iter = iter, closed = closed)

par(mfrow = c(1,1), mar = c(2,2,3,1))
plot(dat$x, dat$y, type = "l", col = 2, asp = 1, axes = FALSE, xlab = "", ylab = "",
     main = if (closed) sprintf("Koch snowflake: iter=%d", iter)
            else sprintf("Koch curve: iter=%d", iter))

############################################################
# Optional: simple zoom (change xlim/ylim)
############################################################
# plot(dat$x, dat$y, type="l", col = 2, asp=1, axes=FALSE, xlab="", ylab="",
#      xlim=c(0.25, 0.45), ylim=c(0.00, 0.20),
#      main=sprintf("Koch curve (zoom): iter=%d", iter))

※ もし記事の中で「ここ違うよ」という点や気になるところがあれば、気軽に指摘していただけると助かります。質問や「このテーマも取り上げてほしい」といったリクエストも大歓迎です。必ず対応するとは約束できませんが、できるだけ今後の記事で扱いたいと思います。それと、下のはてなブログランキングはあまり信用できる指標ではなさそうですが (私のブログを読んでいる人は、実際とても少ないです)、押してもらえるとシンプルに励みになります。気が向いたときにポチッとしていただけたら嬉しいです。