Caffeを使ったCNNによる手書き文字分類 [4] – ネットワークの訓練方法

 前回までで、データセットの準備、ネットワークの構造の説明、各prototxtの準備が整ったので、訓練が行えるようになりました。
訓練の方法は2通りあります。1つは、Caffeeに用意されているプログラムを端末から直接叩くことで、もう1つは、PythonからCaffeを扱えるようにしたPyCaffeと呼ばれるライブラリで訓練する方法です。
今回は簡単のため、前者の方法を紹介します。なお、訓練後の実戦配備(deploy)については、PyCaffeを使ったほうがはるかに簡単で柔軟性があるため、そちらはPythonでやる方法を次の記事で紹介します。
 ここでは、Caffe付属の実行ファイルを使い、訓練できるようになること、また、訓練誤差とテスト誤差の違いを理解すること、誤差だけでなく正答率も出せるようになることが目標です。

フォルダ構成の確認

 まずはフォルダ構成を確認します。現時点で以下のようになっていることが前提です。

4_mycnnfolder.png

自分のフォルダ(/home/ユーザー名/)直下に mycnn というフォルダを置いています。
また、上のようにテスト用の mnist_test_lmdb、訓練用の mnist_train_lmdb が設置されています。
訓練を実施するために solver.prototxt と train.prototxt も設置しました。train.ipynb は無視してください。

訓練の方法

訓練については、Python のライブラリである PyCaffe から行う方法と、Caffe 付属の実行ファイルにコマンドを渡して、Caffe にやってもらう方法の2種類があります。
後者は1行で終って簡単であるため、今回は後者について説明をします。まず、この実行ファイルは端末(黒い画面)で次のように撃ちこめば実行できます。

$HOME/caffe/build/tools/caffe

$HOME は環境変数と呼ばれるもので、 「/home/あなたのユーザー名」 に相当するものを自動で算出してくれます。試しに端末を出した状態で

echo $HOME

と書いてみればそれが分かります。おそらく、「/home/あなたのユーザー名」が表示されたことでしょう。
つまり、このツールは Caffe をインストールしたフォルダにある build の tools に格納されています。
どのようなオプションがあるかは、 Linux のルールにより、用意されていればですが、普通は –help と書けば表示されることになっています。
実際に

$HOME/caffe/build/tools/caffe --help

を実行してみましょう。すると

caffe: command line brew
usage: caffe  
commands:
train           train or finetune a model
test            score a model
device_query    show GPU diagnostic information
time            benchmark model execution time
Flags from src/gflags.cc:
-flagfile (load flags from file) type: string default: ""
-fromenv (set flags from the environment [use 'export FLAGS_flag1=value'])
type: string default: ""
.....

という長々としたヘルプが表示されます。簡単に言うと、 “caffe [やりたいこと] [引数各種]” と書けば訓練できます。
訓練したい場合、前者は train 、後者は –solver=solver.prototxtのパス とすれば良いですね。
 すでに訓練のための準備は整っているので、次のようにして訓練を実行してください。

cd $HOME/mycnn/
$HOME/caffe/build/tools/caffe train --solver=solver.prototxt

すると、黒い画面に色々な文字が流れて、学習が始まると思います。 Optimization Done. と出てくるまでコーヒーでも飲んで待ちましょう。
GeForce GTX TITAN X の場合は1分ほどで終了します。最終的には以下のような表記で終了したはずです。

I0318 15:13:42.657608  3382 layer_factory.hpp:76] Creating layer mnist
I0318 15:13:42.657755  3382 net.cpp:106] Creating Layer mnist
I0318 15:13:42.657778  3382 net.cpp:411] mnist -> data
I0318 15:13:42.657794  3382 net.cpp:411] mnist -> label
I0318 15:13:42.658409  3388 db_lmdb.cpp:38] Opened lmdb mnist_test_lmdb
I0318 15:13:42.658512  3382 data_layer.cpp:41] output data size: 100,1,28,28
I0318 15:13:42.659410  3382 net.cpp:150] Setting up mnist
I0318 15:13:42.659430  3382 net.cpp:157] Top shape: 100 1 28 28 (78400)
I0318 15:13:42.659441  3382 net.cpp:157] Top shape: 100 (100)
I0318 15:13:42.659451  3382 net.cpp:165] Memory required for data: 314000
I0318 15:13:42.659459  3382 layer_factory.hpp:76] Creating layer conv1
I0318 15:13:42.659473  3382 net.cpp:106] Creating Layer conv1
I0318 15:13:42.659483  3382 net.cpp:454] conv1 <- data
I0318 15:13:42.659497  3382 net.cpp:411] conv1 -> conv1
I0318 15:13:42.659677  3382 net.cpp:150] Setting up conv1
I0318 15:13:42.659693  3382 net.cpp:157] Top shape: 100 20 24 24 (1152000)
I0318 15:13:42.659701  3382 net.cpp:165] Memory required for data: 4922000
I0318 15:13:42.659718  3382 layer_factory.hpp:76] Creating layer pool1
I0318 15:13:42.659732  3382 net.cpp:106] Creating Layer pool1
I0318 15:13:42.659742  3382 net.cpp:454] pool1 <- conv1
I0318 15:13:42.659754  3382 net.cpp:411] pool1 -> pool1
I0318 15:13:42.659790  3382 net.cpp:150] Setting up pool1
I0318 15:13:42.659803  3382 net.cpp:157] Top shape: 100 20 12 12 (288000)
I0318 15:13:42.659812  3382 net.cpp:165] Memory required for data: 6074000
I0318 15:13:42.659821  3382 layer_factory.hpp:76] Creating layer conv2
I0318 15:13:42.659834  3382 net.cpp:106] Creating Layer conv2
I0318 15:13:42.659844  3382 net.cpp:454] conv2 <- pool1
I0318 15:13:42.659855  3382 net.cpp:411] conv2 -> conv2
I0318 15:13:42.660253  3382 net.cpp:150] Setting up conv2
I0318 15:13:42.660269  3382 net.cpp:157] Top shape: 100 50 8 8 (320000)
I0318 15:13:42.660279  3382 net.cpp:165] Memory required for data: 7354000
I0318 15:13:42.660298  3382 layer_factory.hpp:76] Creating layer pool2
I0318 15:13:42.660316  3382 net.cpp:106] Creating Layer pool2
I0318 15:13:42.660326  3382 net.cpp:454] pool2 <- conv2
I0318 15:13:42.660341  3382 net.cpp:411] pool2 -> pool2
I0318 15:13:42.660379  3382 net.cpp:150] Setting up pool2
I0318 15:13:42.660393  3382 net.cpp:157] Top shape: 100 50 4 4 (80000)
I0318 15:13:42.660405  3382 net.cpp:165] Memory required for data: 7674000
I0318 15:13:42.660418  3382 layer_factory.hpp:76] Creating layer ip1
I0318 15:13:42.660434  3382 net.cpp:106] Creating Layer ip1
I0318 15:13:42.660446  3382 net.cpp:454] ip1 <- pool2
I0318 15:13:42.660459  3382 net.cpp:411] ip1 -> ip1
I0318 15:13:42.663516  3382 net.cpp:150] Setting up ip1
I0318 15:13:42.663533  3382 net.cpp:157] Top shape: 100 500 (50000)
I0318 15:13:42.663543  3382 net.cpp:165] Memory required for data: 7874000
I0318 15:13:42.663558  3382 layer_factory.hpp:76] Creating layer relu1
I0318 15:13:42.663570  3382 net.cpp:106] Creating Layer relu1
I0318 15:13:42.663579  3382 net.cpp:454] relu1 <- ip1
I0318 15:13:42.663590  3382 net.cpp:397] relu1 -> ip1 (in-place)
I0318 15:13:42.663604  3382 net.cpp:150] Setting up relu1
I0318 15:13:42.663614  3382 net.cpp:157] Top shape: 100 500 (50000)
I0318 15:13:42.663622  3382 net.cpp:165] Memory required for data: 8074000
I0318 15:13:42.663645  3382 layer_factory.hpp:76] Creating layer ip2
I0318 15:13:42.663661  3382 net.cpp:106] Creating Layer ip2
I0318 15:13:42.663671  3382 net.cpp:454] ip2 <- ip1
I0318 15:13:42.663682  3382 net.cpp:411] ip2 -> ip2
I0318 15:13:42.663803  3382 net.cpp:150] Setting up ip2
I0318 15:13:42.663817  3382 net.cpp:157] Top shape: 100 10 (1000)
I0318 15:13:42.663826  3382 net.cpp:165] Memory required for data: 8078000
I0318 15:13:42.663838  3382 layer_factory.hpp:76] Creating layer loss
I0318 15:13:42.663851  3382 net.cpp:106] Creating Layer loss
I0318 15:13:42.663859  3382 net.cpp:454] loss <- ip2
I0318 15:13:42.663869  3382 net.cpp:454] loss <- label
I0318 15:13:42.663880  3382 net.cpp:411] loss -> loss
I0318 15:13:42.663894  3382 layer_factory.hpp:76] Creating layer loss
I0318 15:13:42.663974  3382 net.cpp:150] Setting up loss
I0318 15:13:42.663987  3382 net.cpp:157] Top shape: (1)
I0318 15:13:42.663995  3382 net.cpp:160]     with loss weight 1
I0318 15:13:42.664010  3382 net.cpp:165] Memory required for data: 8078004
I0318 15:13:42.664019  3382 net.cpp:226] loss needs backward computation.
I0318 15:13:42.664028  3382 net.cpp:226] ip2 needs backward computation.
I0318 15:13:42.664036  3382 net.cpp:226] relu1 needs backward computation.
I0318 15:13:42.664044  3382 net.cpp:226] ip1 needs backward computation.
I0318 15:13:42.664053  3382 net.cpp:226] pool2 needs backward computation.
I0318 15:13:42.664062  3382 net.cpp:226] conv2 needs backward computation.
I0318 15:13:42.664072  3382 net.cpp:226] pool1 needs backward computation.
I0318 15:13:42.664079  3382 net.cpp:226] conv1 needs backward computation.
I0318 15:13:42.664088  3382 net.cpp:228] mnist does not need backward computation.
I0318 15:13:42.664098  3382 net.cpp:270] This network produces output loss
I0318 15:13:42.664111  3382 net.cpp:283] Network initialization done.
I0318 15:13:42.664154  3382 solver.cpp:59] Solver scaffolding done.
I0318 15:13:42.664379  3382 caffe.cpp:212] Starting Optimization
I0318 15:13:42.664393  3382 solver.cpp:287] Solving LeNet
I0318 15:13:42.664402  3382 solver.cpp:288] Learning Rate Policy: fixed
I0318 15:13:42.664774  3382 solver.cpp:340] Iteration 0, Testing net (#0)
I0318 15:13:43.801057  3382 solver.cpp:408]     Test net output #0: loss = 2.35588 (* 1 = 2.35588 loss)
I0318 15:13:43.811292  3382 solver.cpp:236] Iteration 0, loss = 2.35228
I0318 15:13:43.811316  3382 solver.cpp:252]     Train net output #0: loss = 2.35228 (* 1 = 2.35228 loss)
I0318 15:13:43.811331  3382 sgd_solver.cpp:106] Iteration 0, lr = 0.01
I0318 15:13:46.753796  3382 solver.cpp:340] Iteration 200, Testing net (#0)
I0318 15:13:47.894870  3382 solver.cpp:408]     Test net output #0: loss = 0.428551 (* 1 = 0.428551 loss)
I0318 15:13:47.904305  3382 solver.cpp:236] Iteration 200, loss = 0.352812
I0318 15:13:47.904328  3382 solver.cpp:252]     Train net output #0: loss = 0.352812 (* 1 = 0.352812 loss)
I0318 15:13:47.904341  3382 sgd_solver.cpp:106] Iteration 200, lr = 0.01
I0318 15:13:50.821164  3382 solver.cpp:340] Iteration 400, Testing net (#0)
I0318 15:13:51.916532  3382 solver.cpp:408]     Test net output #0: loss = 0.295521 (* 1 = 0.295521 loss)
I0318 15:13:51.925935  3382 solver.cpp:236] Iteration 400, loss = 0.235877
I0318 15:13:51.925959  3382 solver.cpp:252]     Train net output #0: loss = 0.235877 (* 1 = 0.235877 loss)
I0318 15:13:51.925971  3382 sgd_solver.cpp:106] Iteration 400, lr = 0.01
I0318 15:13:54.819229  3382 solver.cpp:340] Iteration 600, Testing net (#0)
I0318 15:13:55.914777  3382 solver.cpp:408]     Test net output #0: loss = 0.26432 (* 1 = 0.26432 loss)
I0318 15:13:55.924188  3382 solver.cpp:236] Iteration 600, loss = 0.182522
I0318 15:13:55.924212  3382 solver.cpp:252]     Train net output #0: loss = 0.182522 (* 1 = 0.182522 loss)
I0318 15:13:55.924226  3382 sgd_solver.cpp:106] Iteration 600, lr = 0.01
I0318 15:13:58.818703  3382 solver.cpp:340] Iteration 800, Testing net (#0)
I0318 15:13:59.913530  3382 solver.cpp:408]     Test net output #0: loss = 0.201615 (* 1 = 0.201615 loss)
I0318 15:13:59.923005  3382 solver.cpp:236] Iteration 800, loss = 0.269571
I0318 15:13:59.923058  3382 solver.cpp:252]     Train net output #0: loss = 0.269571 (* 1 = 0.269571 loss)
I0318 15:13:59.923071  3382 sgd_solver.cpp:106] Iteration 800, lr = 0.01
I0318 15:14:02.819603  3382 solver.cpp:340] Iteration 1000, Testing net (#0)
I0318 15:14:03.916120  3382 solver.cpp:408]     Test net output #0: loss = 0.177828 (* 1 = 0.177828 loss)
I0318 15:14:03.925526  3382 solver.cpp:236] Iteration 1000, loss = 0.173842
I0318 15:14:03.925549  3382 solver.cpp:252]     Train net output #0: loss = 0.173842 (* 1 = 0.173842 loss)
I0318 15:14:03.925570  3382 sgd_solver.cpp:106] Iteration 1000, lr = 0.01
I0318 15:14:06.818325  3382 solver.cpp:340] Iteration 1200, Testing net (#0)
I0318 15:14:07.915375  3382 solver.cpp:408]     Test net output #0: loss = 0.155351 (* 1 = 0.155351 loss)
I0318 15:14:07.924793  3382 solver.cpp:236] Iteration 1200, loss = 0.108761
I0318 15:14:07.924816  3382 solver.cpp:252]     Train net output #0: loss = 0.108761 (* 1 = 0.108761 loss)
I0318 15:14:07.924837  3382 sgd_solver.cpp:106] Iteration 1200, lr = 0.01
I0318 15:14:10.823040  3382 solver.cpp:340] Iteration 1400, Testing net (#0)
I0318 15:14:11.942628  3382 solver.cpp:408]     Test net output #0: loss = 0.148154 (* 1 = 0.148154 loss)
I0318 15:14:11.952159  3382 solver.cpp:236] Iteration 1400, loss = 0.0609879
I0318 15:14:11.952183  3382 solver.cpp:252]     Train net output #0: loss = 0.0609879 (* 1 = 0.0609879 loss)
I0318 15:14:11.952203  3382 sgd_solver.cpp:106] Iteration 1400, lr = 0.01
I0318 15:14:14.873962  3382 solver.cpp:340] Iteration 1600, Testing net (#0)
I0318 15:14:15.992725  3382 solver.cpp:408]     Test net output #0: loss = 0.131436 (* 1 = 0.131436 loss)
I0318 15:14:16.002230  3382 solver.cpp:236] Iteration 1600, loss = 0.291724
I0318 15:14:16.002254  3382 solver.cpp:252]     Train net output #0: loss = 0.291725 (* 1 = 0.291725 loss)
I0318 15:14:16.002274  3382 sgd_solver.cpp:106] Iteration 1600, lr = 0.01
I0318 15:14:18.922446  3382 solver.cpp:340] Iteration 1800, Testing net (#0)
I0318 15:14:20.039000  3382 solver.cpp:408]     Test net output #0: loss = 0.129016 (* 1 = 0.129016 loss)
I0318 15:14:20.048508  3382 solver.cpp:236] Iteration 1800, loss = 0.0556022
I0318 15:14:20.048532  3382 solver.cpp:252]     Train net output #0: loss = 0.0556022 (* 1 = 0.0556022 loss)
I0318 15:14:20.048547  3382 sgd_solver.cpp:106] Iteration 1800, lr = 0.01
I0318 15:14:22.970935  3382 solver.cpp:340] Iteration 2000, Testing net (#0)
I0318 15:14:24.088537  3382 solver.cpp:408]     Test net output #0: loss = 0.114576 (* 1 = 0.114576 loss)
I0318 15:14:24.098008  3382 solver.cpp:236] Iteration 2000, loss = 0.110296
I0318 15:14:24.098032  3382 solver.cpp:252]     Train net output #0: loss = 0.110296 (* 1 = 0.110296 loss)
I0318 15:14:24.098045  3382 sgd_solver.cpp:106] Iteration 2000, lr = 0.01
I0318 15:14:27.021292  3382 solver.cpp:340] Iteration 2200, Testing net (#0)
I0318 15:14:28.140019  3382 solver.cpp:408]     Test net output #0: loss = 0.11897 (* 1 = 0.11897 loss)
I0318 15:14:28.149525  3382 solver.cpp:236] Iteration 2200, loss = 0.109589
I0318 15:14:28.149549  3382 solver.cpp:252]     Train net output #0: loss = 0.109589 (* 1 = 0.109589 loss)
I0318 15:14:28.149562  3382 sgd_solver.cpp:106] Iteration 2200, lr = 0.01
I0318 15:14:31.071166  3382 solver.cpp:340] Iteration 2400, Testing net (#0)
I0318 15:14:32.189035  3382 solver.cpp:408]     Test net output #0: loss = 0.0957816 (* 1 = 0.0957816 loss)
I0318 15:14:32.198529  3382 solver.cpp:236] Iteration 2400, loss = 0.0452349
I0318 15:14:32.198552  3382 solver.cpp:252]     Train net output #0: loss = 0.0452348 (* 1 = 0.0452348 loss)
I0318 15:14:32.198565  3382 sgd_solver.cpp:106] Iteration 2400, lr = 0.01
I0318 15:14:35.120736  3382 solver.cpp:340] Iteration 2600, Testing net (#0)
I0318 15:14:36.240253  3382 solver.cpp:408]     Test net output #0: loss = 0.0962278 (* 1 = 0.0962278 loss)
I0318 15:14:36.249775  3382 solver.cpp:236] Iteration 2600, loss = 0.22706
I0318 15:14:36.249799  3382 solver.cpp:252]     Train net output #0: loss = 0.227059 (* 1 = 0.227059 loss)
I0318 15:14:36.249812  3382 sgd_solver.cpp:106] Iteration 2600, lr = 0.01
I0318 15:14:39.172605  3382 solver.cpp:340] Iteration 2800, Testing net (#0)
I0318 15:14:40.348649  3382 solver.cpp:408]     Test net output #0: loss = 0.0932576 (* 1 = 0.0932576 loss)
I0318 15:14:40.358490  3382 solver.cpp:236] Iteration 2800, loss = 0.00793065
I0318 15:14:40.358517  3382 solver.cpp:252]     Train net output #0: loss = 0.00793056 (* 1 = 0.00793056 loss)
I0318 15:14:40.358532  3382 sgd_solver.cpp:106] Iteration 2800, lr = 0.01
I0318 15:14:43.394129  3382 solver.cpp:340] Iteration 3000, Testing net (#0)
I0318 15:14:44.574527  3382 solver.cpp:408]     Test net output #0: loss = 0.102128 (* 1 = 0.102128 loss)
I0318 15:14:44.584008  3382 solver.cpp:236] Iteration 3000, loss = 0.0599364
I0318 15:14:44.584033  3382 solver.cpp:252]     Train net output #0: loss = 0.0599362 (* 1 = 0.0599362 loss)
I0318 15:14:44.584053  3382 sgd_solver.cpp:106] Iteration 3000, lr = 0.01
I0318 15:14:47.570310  3382 solver.cpp:340] Iteration 3200, Testing net (#0)
I0318 15:14:48.688124  3382 solver.cpp:408]     Test net output #0: loss = 0.0781087 (* 1 = 0.0781087 loss)
I0318 15:14:48.697677  3382 solver.cpp:236] Iteration 3200, loss = 0.0838349
I0318 15:14:48.697715  3382 solver.cpp:252]     Train net output #0: loss = 0.0838348 (* 1 = 0.0838348 loss)
I0318 15:14:48.697736  3382 sgd_solver.cpp:106] Iteration 3200, lr = 0.01
I0318 15:14:51.668588  3382 solver.cpp:340] Iteration 3400, Testing net (#0)
I0318 15:14:52.804702  3382 solver.cpp:408]     Test net output #0: loss = 0.08053 (* 1 = 0.08053 loss)
I0318 15:14:52.814335  3382 solver.cpp:236] Iteration 3400, loss = 0.0446551
I0318 15:14:52.814364  3382 solver.cpp:252]     Train net output #0: loss = 0.044655 (* 1 = 0.044655 loss)
I0318 15:14:52.814380  3382 sgd_solver.cpp:106] Iteration 3400, lr = 0.01
I0318 15:14:55.794299  3382 solver.cpp:340] Iteration 3600, Testing net (#0)
I0318 15:14:56.917431  3382 solver.cpp:408]     Test net output #0: loss = 0.0822716 (* 1 = 0.0822716 loss)
I0318 15:14:56.927007  3382 solver.cpp:236] Iteration 3600, loss = 0.122091
I0318 15:14:56.927032  3382 solver.cpp:252]     Train net output #0: loss = 0.12209 (* 1 = 0.12209 loss)
I0318 15:14:56.927044  3382 sgd_solver.cpp:106] Iteration 3600, lr = 0.01
I0318 15:14:59.936321  3382 solver.cpp:340] Iteration 3800, Testing net (#0)
I0318 15:15:01.064332  3382 solver.cpp:408]     Test net output #0: loss = 0.0712671 (* 1 = 0.0712671 loss)
I0318 15:15:01.074139  3382 solver.cpp:236] Iteration 3800, loss = 0.0471237
I0318 15:15:01.074173  3382 solver.cpp:252]     Train net output #0: loss = 0.0471236 (* 1 = 0.0471236 loss)
I0318 15:15:01.074193  3382 sgd_solver.cpp:106] Iteration 3800, lr = 0.01
I0318 15:15:04.068575  3382 solver.cpp:340] Iteration 4000, Testing net (#0)
I0318 15:15:05.192591  3382 solver.cpp:408]     Test net output #0: loss = 0.0669779 (* 1 = 0.0669779 loss)
I0318 15:15:05.202126  3382 solver.cpp:236] Iteration 4000, loss = 0.0956275
I0318 15:15:05.202148  3382 solver.cpp:252]     Train net output #0: loss = 0.0956274 (* 1 = 0.0956274 loss)
I0318 15:15:05.202168  3382 sgd_solver.cpp:106] Iteration 4000, lr = 0.01
I0318 15:15:08.132586  3382 solver.cpp:340] Iteration 4200, Testing net (#0)
I0318 15:15:09.258705  3382 solver.cpp:408]     Test net output #0: loss = 0.0672639 (* 1 = 0.0672639 loss)
I0318 15:15:09.268530  3382 solver.cpp:236] Iteration 4200, loss = 0.0474286
I0318 15:15:09.268558  3382 solver.cpp:252]     Train net output #0: loss = 0.0474285 (* 1 = 0.0474285 loss)
I0318 15:15:09.268571  3382 sgd_solver.cpp:106] Iteration 4200, lr = 0.01
I0318 15:15:12.210129  3382 solver.cpp:340] Iteration 4400, Testing net (#0)
I0318 15:15:13.332058  3382 solver.cpp:408]     Test net output #0: loss = 0.0638991 (* 1 = 0.0638991 loss)
I0318 15:15:13.341629  3382 solver.cpp:236] Iteration 4400, loss = 0.0803695
I0318 15:15:13.341652  3382 solver.cpp:252]     Train net output #0: loss = 0.0803694 (* 1 = 0.0803694 loss)
I0318 15:15:13.341665  3382 sgd_solver.cpp:106] Iteration 4400, lr = 0.01
I0318 15:15:16.278879  3382 solver.cpp:340] Iteration 4600, Testing net (#0)
I0318 15:15:17.401496  3382 solver.cpp:408]     Test net output #0: loss = 0.0639758 (* 1 = 0.0639758 loss)
I0318 15:15:17.411195  3382 solver.cpp:236] Iteration 4600, loss = 0.0269316
I0318 15:15:17.411221  3382 solver.cpp:252]     Train net output #0: loss = 0.0269316 (* 1 = 0.0269316 loss)
I0318 15:15:17.411232  3382 sgd_solver.cpp:106] Iteration 4600, lr = 0.01
I0318 15:15:20.382869  3382 solver.cpp:340] Iteration 4800, Testing net (#0)
I0318 15:15:21.502028  3382 solver.cpp:408]     Test net output #0: loss = 0.0620971 (* 1 = 0.0620971 loss)
I0318 15:15:21.511581  3382 solver.cpp:236] Iteration 4800, loss = 0.12216
I0318 15:15:21.511605  3382 solver.cpp:252]     Train net output #0: loss = 0.12216 (* 1 = 0.12216 loss)
I0318 15:15:21.511618  3382 sgd_solver.cpp:106] Iteration 4800, lr = 0.01
I0318 15:15:24.461321  3382 solver.cpp:461] Snapshotting to binary proto file _iter_5000.caffemodel
I0318 15:15:24.473604  3382 sgd_solver.cpp:269] Snapshotting solver state to binary proto file _iter_5000.solverstate
I0318 15:15:24.483548  3382 solver.cpp:320] Iteration 5000, loss = 0.0696701
I0318 15:15:24.483579  3382 solver.cpp:340] Iteration 5000, Testing net (#0)
I0318 15:15:25.610076  3382 solver.cpp:408]     Test net output #0: loss = 0.058336 (* 1 = 0.058336 loss)
I0318 15:15:25.610106  3382 solver.cpp:325] Optimization Done.
I0318 15:15:25.610116  3382 caffe.cpp:215] Optimization Done.

このように、指定された display の数ごとに、訓練中のネットワークの誤差が loss として出力されています。一方で、テストは test_interval で指定した間隔ごとに実施され、その時の誤差もあわせて報告されています。
訓練用ネットワークの誤差は Train net output: の時の loss を、テスト用の場合は Test net output: の時の loss を見ればよいですね。
 以上の出力内容から、最初は2.30あったテスト誤差が、最終的には0.05といった値まで小さくなっていることが理解できます。
なお、誤差の絶対的な値の大きさには特に大きな意味はないので、最初の誤差よりも小さくなっていることを確認できたということが重要です。
 また、 max_iter に達すると、その時のネットワークの状態を保存したファイル(solverstate, caffemodel)がそれぞれ生成されます。指定がない場合、「_iter_最後の回数.caffemodel」といった名前になっています。
solverstate はそこからさらに学習するための保存データであり、 caffemodel は学習済みモデルとして実戦配備(deploy)するための保存データです。
この結果に満足して、実際に自分でペイントなどで書いた数字を判別させたいときは caffemodel を使います。
要するに solverstate はただの訓練時の中断セーブデータであり、 caffemodel はそこで訓練を終了した、すぐに一般画像に対して使用可能な完成データです。

訓練誤差とテスト誤差

 先ほど見てきた訓練用ネットワークの誤差関数の値は訓練誤差、テスト用ネットワークの誤差関数の値はテスト誤差と呼ばれています。
ここで、訓練誤差とテスト誤差をわざわざ両方表示させた意味について考えてみましょう。
 訓練誤差は、その名の通り訓練データから取り出した現在のバッチに対する誤差です。この誤差をもとに直接パラメータ更新を行うので、訓練誤差は基本的に「減って当たり前」の指標です。
もちろん、学習率や学習データ、構造のなどの兼ね合いで0にはなりませんが、少なくとも大きな欠陥がない限り、ある程度は訓練誤差は減ります。
くどいようですが、訓練データに対して誤差が減ることを目標としたパラメータ更新のかけかたをしているからです。
 一方でテスト誤差は、学習の直接的な対象になっているデータ、つまり訓練データとは全く違うデータに対する誤差です。なので、テスト誤差は訓練誤差と違い、必ずしも減少することは保証されません。
これが訓練誤差との大きな違いで、意識すべき点です。言い換えれば、訓練誤差が減っていても、テスト誤差も減っていなければ意味がなく、「正しく学習ができた」とは決して言えません。
 ここから「過学習」を別の視点で見ることができます。

このように、訓練時のバッチ数を経るごとに、テストもそれに応じて実施したとすると、誤差の時間的推移をグラフにすることができます。
もし理想的に訓練ができているなら、訓練誤差とテスト誤差はほぼ同じ水準で推移するはずです。
しかしながら、上のように、テスト誤差が訓練誤差を大幅に上回り減らないような状況に陥ることがしばしばあります。
これが過学習が起きている場合の典型的な状況です。
 先ほどの場合、訓練誤差もテスト誤差も同程度に減少しているため、訓練データのみならず、未知のデータに対しても対応できるよう、うまく訓練ができたと考えられます。

正答率の算出

これまでは誤差が減ることを確認してきましたが、これはうまく訓練できたことの「目安」に過ぎません。
結局のところ、誤差が小さくなったのは素晴らしいことですが、あなたが「誤差が0.05になった」とみんなに自慢しても、一体それがどれくらいうまくいっているもので、どう凄いのか、人間には何の見当もつかないのです。
それどころか、このネットワークがどれくらいできる子であるのかは、あなたにもわからないことでしょう。
我々が知りたいのは、パラメータを上手く更新するために導入された誤差というより、実際にどれくらいの割合で正解しているのかを示す指標です。つまり、実用上の直接的な指標に成りうる、正答率(accuracy)を算出したいのです。
 誤差の出し方は簡単です。ネットワークの出力ユニットのうち、最大のユニットと、正解ラベルを比較して、どのくらいそれが一致しているかを調べれば原理的には分かります。
Pythonでは簡単にこれが書けますが、今回はCaffeの実行ファイルでやりたいので、別の方法を探します。
大変幸運なことに、Caffeでは、この上で書いたような正答率を自動で算出してくれる層、 Accuracy層というものが用意されています。
Caffeは便宜上誤差関数や活性化関数、この正答率もlayer、つまり見かけ上層として定義しますが、ニューラルネットの構造上層に含まれないというのはすでに前回も説明しました。
ここではCaffeでlayerとして定義するものはとりあえず「層」と称しておきますが、注意しましょう。
 早速train.prototxtの最後に次の記述を追加しましょう。

layer {
name: "accuracy"
type: "Accuracy"
bottom: "ip2"
bottom: "label"
top: "accuracy"
}

書式は loss と一緒で、ネットワークの出力層を最初の bottom に、入力層から持ってきた正解ラベルを次の bottom に指定し、その正答率の出力名を top として指定します。
これで、テスト時の正答率、訓練時の正答率が表示されるようになります。念の為、それを反映した train.prototxt を以下からダウンロードできるようにしておきます。

train.prototxt

これで同じように訓練を実行すると、次のような結果が得られたことと思います。

I0318 15:49:35.439347  3487 caffe.cpp:212] Starting Optimization
I0318 15:49:35.439359  3487 solver.cpp:287] Solving LeNet
I0318 15:49:35.439368  3487 solver.cpp:288] Learning Rate Policy: fixed
I0318 15:49:35.439735  3487 solver.cpp:340] Iteration 0, Testing net (#0)
I0318 15:49:36.599722  3487 solver.cpp:408]     Test net output #0: accuracy = 0.0753
I0318 15:49:36.599763  3487 solver.cpp:408]     Test net output #1: loss = 2.39124 (* 1 = 2.39124 loss)
I0318 15:49:36.610008  3487 solver.cpp:236] Iteration 0, loss = 2.37326
I0318 15:49:36.610028  3487 solver.cpp:252]     Train net output #0: accuracy = 0.109375
I0318 15:49:36.610044  3487 solver.cpp:252]     Train net output #1: loss = 2.37326 (* 1 = 2.37326 loss)
I0318 15:49:36.610056  3487 sgd_solver.cpp:106] Iteration 0, lr = 0.01
I0318 15:49:39.517323  3487 solver.cpp:340] Iteration 200, Testing net (#0)
I0318 15:49:40.628067  3487 solver.cpp:408]     Test net output #0: accuracy = 0.8854
I0318 15:49:40.628106  3487 solver.cpp:408]     Test net output #1: loss = 0.426777 (* 1 = 0.426777 loss)
I0318 15:49:40.637663  3487 solver.cpp:236] Iteration 200, loss = 0.385224
I0318 15:49:40.637682  3487 solver.cpp:252]     Train net output #0: accuracy = 0.859375
I0318 15:49:40.637696  3487 solver.cpp:252]     Train net output #1: loss = 0.385224 (* 1 = 0.385224 loss)
I0318 15:49:40.637706  3487 sgd_solver.cpp:106] Iteration 200, lr = 0.01
I0318 15:49:43.707788  3487 solver.cpp:340] Iteration 400, Testing net (#0)
I0318 15:49:44.952904  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9158
I0318 15:49:44.952952  3487 solver.cpp:408]     Test net output #1: loss = 0.300111 (* 1 = 0.300111 loss)
I0318 15:49:44.962651  3487 solver.cpp:236] Iteration 400, loss = 0.23237
I0318 15:49:44.962678  3487 solver.cpp:252]     Train net output #0: accuracy = 0.921875
I0318 15:49:44.962702  3487 solver.cpp:252]     Train net output #1: loss = 0.23237 (* 1 = 0.23237 loss)
I0318 15:49:44.962713  3487 sgd_solver.cpp:106] Iteration 400, lr = 0.01
I0318 15:49:48.135368  3487 solver.cpp:340] Iteration 600, Testing net (#0)
I0318 15:49:49.241222  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9212
I0318 15:49:49.241266  3487 solver.cpp:408]     Test net output #1: loss = 0.258399 (* 1 = 0.258399 loss)
I0318 15:49:49.250757  3487 solver.cpp:236] Iteration 600, loss = 0.187386
I0318 15:49:49.250776  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:49:49.250789  3487 solver.cpp:252]     Train net output #1: loss = 0.187386 (* 1 = 0.187386 loss)
I0318 15:49:49.250800  3487 sgd_solver.cpp:106] Iteration 600, lr = 0.01
I0318 15:49:52.166229  3487 solver.cpp:340] Iteration 800, Testing net (#0)
I0318 15:49:53.302460  3487 solver.cpp:408]     Test net output #0: accuracy = 0.94
I0318 15:49:53.302505  3487 solver.cpp:408]     Test net output #1: loss = 0.204675 (* 1 = 0.204675 loss)
I0318 15:49:53.313238  3487 solver.cpp:236] Iteration 800, loss = 0.268695
I0318 15:49:53.313271  3487 solver.cpp:252]     Train net output #0: accuracy = 0.890625
I0318 15:49:53.313285  3487 solver.cpp:252]     Train net output #1: loss = 0.268695 (* 1 = 0.268695 loss)
I0318 15:49:53.313297  3487 sgd_solver.cpp:106] Iteration 800, lr = 0.01
I0318 15:49:56.422940  3487 solver.cpp:340] Iteration 1000, Testing net (#0)
I0318 15:49:57.551894  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9497
I0318 15:49:57.551937  3487 solver.cpp:408]     Test net output #1: loss = 0.174766 (* 1 = 0.174766 loss)
I0318 15:49:57.561357  3487 solver.cpp:236] Iteration 1000, loss = 0.176971
I0318 15:49:57.561375  3487 solver.cpp:252]     Train net output #0: accuracy = 0.921875
I0318 15:49:57.561388  3487 solver.cpp:252]     Train net output #1: loss = 0.176971 (* 1 = 0.176971 loss)
I0318 15:49:57.561399  3487 sgd_solver.cpp:106] Iteration 1000, lr = 0.01
I0318 15:50:00.478062  3487 solver.cpp:340] Iteration 1200, Testing net (#0)
I0318 15:50:01.584522  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9567
I0318 15:50:01.584563  3487 solver.cpp:408]     Test net output #1: loss = 0.153239 (* 1 = 0.153239 loss)
I0318 15:50:01.594053  3487 solver.cpp:236] Iteration 1200, loss = 0.107422
I0318 15:50:01.594072  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:50:01.594086  3487 solver.cpp:252]     Train net output #1: loss = 0.107422 (* 1 = 0.107422 loss)
I0318 15:50:01.594099  3487 sgd_solver.cpp:106] Iteration 1200, lr = 0.01
I0318 15:50:04.536718  3487 solver.cpp:340] Iteration 1400, Testing net (#0)
I0318 15:50:05.648345  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9607
I0318 15:50:05.648490  3487 solver.cpp:408]     Test net output #1: loss = 0.143523 (* 1 = 0.143523 loss)
I0318 15:50:05.658092  3487 solver.cpp:236] Iteration 1400, loss = 0.070514
I0318 15:50:05.658118  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:50:05.658133  3487 solver.cpp:252]     Train net output #1: loss = 0.070514 (* 1 = 0.070514 loss)
I0318 15:50:05.658144  3487 sgd_solver.cpp:106] Iteration 1400, lr = 0.01
I0318 15:50:08.604581  3487 solver.cpp:340] Iteration 1600, Testing net (#0)
I0318 15:50:09.768698  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9606
I0318 15:50:09.768741  3487 solver.cpp:408]     Test net output #1: loss = 0.138688 (* 1 = 0.138688 loss)
I0318 15:50:09.778146  3487 solver.cpp:236] Iteration 1600, loss = 0.301987
I0318 15:50:09.778169  3487 solver.cpp:252]     Train net output #0: accuracy = 0.90625
I0318 15:50:09.778192  3487 solver.cpp:252]     Train net output #1: loss = 0.301987 (* 1 = 0.301987 loss)
I0318 15:50:09.778203  3487 sgd_solver.cpp:106] Iteration 1600, lr = 0.01
I0318 15:50:12.721812  3487 solver.cpp:340] Iteration 1800, Testing net (#0)
I0318 15:50:13.858204  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9628
I0318 15:50:13.858247  3487 solver.cpp:408]     Test net output #1: loss = 0.131014 (* 1 = 0.131014 loss)
I0318 15:50:13.867835  3487 solver.cpp:236] Iteration 1800, loss = 0.061839
I0318 15:50:13.867854  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:50:13.867877  3487 solver.cpp:252]     Train net output #1: loss = 0.0618389 (* 1 = 0.0618389 loss)
I0318 15:50:13.867887  3487 sgd_solver.cpp:106] Iteration 1800, lr = 0.01
I0318 15:50:16.832558  3487 solver.cpp:340] Iteration 2000, Testing net (#0)
I0318 15:50:17.974463  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9657
I0318 15:50:17.974515  3487 solver.cpp:408]     Test net output #1: loss = 0.116509 (* 1 = 0.116509 loss)
I0318 15:50:17.984093  3487 solver.cpp:236] Iteration 2000, loss = 0.0983078
I0318 15:50:17.984113  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:50:17.984127  3487 solver.cpp:252]     Train net output #1: loss = 0.0983077 (* 1 = 0.0983077 loss)
I0318 15:50:17.984139  3487 sgd_solver.cpp:106] Iteration 2000, lr = 0.01
I0318 15:50:20.956470  3487 solver.cpp:340] Iteration 2200, Testing net (#0)
I0318 15:50:22.085554  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9634
I0318 15:50:22.085597  3487 solver.cpp:408]     Test net output #1: loss = 0.119031 (* 1 = 0.119031 loss)
I0318 15:50:22.095201  3487 solver.cpp:236] Iteration 2200, loss = 0.122837
I0318 15:50:22.095221  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:50:22.095235  3487 solver.cpp:252]     Train net output #1: loss = 0.122837 (* 1 = 0.122837 loss)
I0318 15:50:22.095247  3487 sgd_solver.cpp:106] Iteration 2200, lr = 0.01
I0318 15:50:25.087008  3487 solver.cpp:340] Iteration 2400, Testing net (#0)
I0318 15:50:26.217252  3487 solver.cpp:408]     Test net output #0: accuracy = 0.971
I0318 15:50:26.217293  3487 solver.cpp:408]     Test net output #1: loss = 0.0998345 (* 1 = 0.0998345 loss)
I0318 15:50:26.226907  3487 solver.cpp:236] Iteration 2400, loss = 0.0464337
I0318 15:50:26.226927  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:50:26.226943  3487 solver.cpp:252]     Train net output #1: loss = 0.0464336 (* 1 = 0.0464336 loss)
I0318 15:50:26.226956  3487 sgd_solver.cpp:106] Iteration 2400, lr = 0.01
I0318 15:50:29.160692  3487 solver.cpp:340] Iteration 2600, Testing net (#0)
I0318 15:50:30.286012  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9693
I0318 15:50:30.286054  3487 solver.cpp:408]     Test net output #1: loss = 0.100496 (* 1 = 0.100496 loss)
I0318 15:50:30.295588  3487 solver.cpp:236] Iteration 2600, loss = 0.231967
I0318 15:50:30.295606  3487 solver.cpp:252]     Train net output #0: accuracy = 0.9375
I0318 15:50:30.295629  3487 solver.cpp:252]     Train net output #1: loss = 0.231966 (* 1 = 0.231966 loss)
I0318 15:50:30.295639  3487 sgd_solver.cpp:106] Iteration 2600, lr = 0.01
I0318 15:50:33.266854  3487 solver.cpp:340] Iteration 2800, Testing net (#0)
I0318 15:50:34.404389  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9701
I0318 15:50:34.404433  3487 solver.cpp:408]     Test net output #1: loss = 0.0984428 (* 1 = 0.0984428 loss)
I0318 15:50:34.414026  3487 solver.cpp:236] Iteration 2800, loss = 0.00701111
I0318 15:50:34.414044  3487 solver.cpp:252]     Train net output #0: accuracy = 1
I0318 15:50:34.414058  3487 solver.cpp:252]     Train net output #1: loss = 0.00701095 (* 1 = 0.00701095 loss)
I0318 15:50:34.414072  3487 sgd_solver.cpp:106] Iteration 2800, lr = 0.01
I0318 15:50:37.345834  3487 solver.cpp:340] Iteration 3000, Testing net (#0)
I0318 15:50:38.494946  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9672
I0318 15:50:38.494997  3487 solver.cpp:408]     Test net output #1: loss = 0.10778 (* 1 = 0.10778 loss)
I0318 15:50:38.504477  3487 solver.cpp:236] Iteration 3000, loss = 0.111363
I0318 15:50:38.504495  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:50:38.504518  3487 solver.cpp:252]     Train net output #1: loss = 0.111363 (* 1 = 0.111363 loss)
I0318 15:50:38.504529  3487 sgd_solver.cpp:106] Iteration 3000, lr = 0.01
I0318 15:50:41.457626  3487 solver.cpp:340] Iteration 3200, Testing net (#0)
I0318 15:50:42.603052  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9751
I0318 15:50:42.603094  3487 solver.cpp:408]     Test net output #1: loss = 0.0839751 (* 1 = 0.0839751 loss)
I0318 15:50:42.612717  3487 solver.cpp:236] Iteration 3200, loss = 0.050921
I0318 15:50:42.612737  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:50:42.612751  3487 solver.cpp:252]     Train net output #1: loss = 0.0509207 (* 1 = 0.0509207 loss)
I0318 15:50:42.612762  3487 sgd_solver.cpp:106] Iteration 3200, lr = 0.01
I0318 15:50:45.554730  3487 solver.cpp:340] Iteration 3400, Testing net (#0)
I0318 15:50:46.702071  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9736
I0318 15:50:46.702113  3487 solver.cpp:408]     Test net output #1: loss = 0.0880515 (* 1 = 0.0880515 loss)
I0318 15:50:46.711730  3487 solver.cpp:236] Iteration 3400, loss = 0.0499841
I0318 15:50:46.711750  3487 solver.cpp:252]     Train net output #0: accuracy = 1
I0318 15:50:46.711763  3487 solver.cpp:252]     Train net output #1: loss = 0.0499839 (* 1 = 0.0499839 loss)
I0318 15:50:46.711777  3487 sgd_solver.cpp:106] Iteration 3400, lr = 0.01
I0318 15:50:49.650635  3487 solver.cpp:340] Iteration 3600, Testing net (#0)
I0318 15:50:50.776473  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9728
I0318 15:50:50.776513  3487 solver.cpp:408]     Test net output #1: loss = 0.0872698 (* 1 = 0.0872698 loss)
I0318 15:50:50.786027  3487 solver.cpp:236] Iteration 3600, loss = 0.137819
I0318 15:50:50.786046  3487 solver.cpp:252]     Train net output #0: accuracy = 0.9375
I0318 15:50:50.786068  3487 solver.cpp:252]     Train net output #1: loss = 0.137819 (* 1 = 0.137819 loss)
I0318 15:50:50.786079  3487 sgd_solver.cpp:106] Iteration 3600, lr = 0.01
I0318 15:50:53.725615  3487 solver.cpp:340] Iteration 3800, Testing net (#0)
I0318 15:50:54.864505  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9769
I0318 15:50:54.864557  3487 solver.cpp:408]     Test net output #1: loss = 0.0733028 (* 1 = 0.0733028 loss)
I0318 15:50:54.874624  3487 solver.cpp:236] Iteration 3800, loss = 0.0461295
I0318 15:50:54.874644  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:50:54.874657  3487 solver.cpp:252]     Train net output #1: loss = 0.0461295 (* 1 = 0.0461295 loss)
I0318 15:50:54.874667  3487 sgd_solver.cpp:106] Iteration 3800, lr = 0.01
I0318 15:50:57.826640  3487 solver.cpp:340] Iteration 4000, Testing net (#0)
I0318 15:50:58.953855  3487 solver.cpp:408]     Test net output #0: accuracy = 0.976
I0318 15:50:58.953897  3487 solver.cpp:408]     Test net output #1: loss = 0.0742581 (* 1 = 0.0742581 loss)
I0318 15:50:58.963500  3487 solver.cpp:236] Iteration 4000, loss = 0.109244
I0318 15:50:58.963520  3487 solver.cpp:252]     Train net output #0: accuracy = 0.953125
I0318 15:50:58.963533  3487 solver.cpp:252]     Train net output #1: loss = 0.109244 (* 1 = 0.109244 loss)
I0318 15:50:58.963546  3487 sgd_solver.cpp:106] Iteration 4000, lr = 0.01
I0318 15:51:01.900107  3487 solver.cpp:340] Iteration 4200, Testing net (#0)
I0318 15:51:03.026459  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9772
I0318 15:51:03.026500  3487 solver.cpp:408]     Test net output #1: loss = 0.0725421 (* 1 = 0.0725421 loss)
I0318 15:51:03.036160  3487 solver.cpp:236] Iteration 4200, loss = 0.0680813
I0318 15:51:03.036180  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:51:03.036195  3487 solver.cpp:252]     Train net output #1: loss = 0.0680811 (* 1 = 0.0680811 loss)
I0318 15:51:03.036233  3487 sgd_solver.cpp:106] Iteration 4200, lr = 0.01
I0318 15:51:05.996453  3487 solver.cpp:340] Iteration 4400, Testing net (#0)
I0318 15:51:07.130614  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9776
I0318 15:51:07.130656  3487 solver.cpp:408]     Test net output #1: loss = 0.072042 (* 1 = 0.072042 loss)
I0318 15:51:07.140278  3487 solver.cpp:236] Iteration 4400, loss = 0.075286
I0318 15:51:07.140300  3487 solver.cpp:252]     Train net output #0: accuracy = 0.96875
I0318 15:51:07.140313  3487 solver.cpp:252]     Train net output #1: loss = 0.0752859 (* 1 = 0.0752859 loss)
I0318 15:51:07.140326  3487 sgd_solver.cpp:106] Iteration 4400, lr = 0.01
I0318 15:51:10.100457  3487 solver.cpp:340] Iteration 4600, Testing net (#0)
I0318 15:51:11.233594  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9794
I0318 15:51:11.233638  3487 solver.cpp:408]     Test net output #1: loss = 0.0656571 (* 1 = 0.0656571 loss)
I0318 15:51:11.243300  3487 solver.cpp:236] Iteration 4600, loss = 0.0248723
I0318 15:51:11.243322  3487 solver.cpp:252]     Train net output #0: accuracy = 1
I0318 15:51:11.243336  3487 solver.cpp:252]     Train net output #1: loss = 0.0248722 (* 1 = 0.0248722 loss)
I0318 15:51:11.243350  3487 sgd_solver.cpp:106] Iteration 4600, lr = 0.01
I0318 15:51:14.214659  3487 solver.cpp:340] Iteration 4800, Testing net (#0)
I0318 15:51:15.341974  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9799
I0318 15:51:15.342015  3487 solver.cpp:408]     Test net output #1: loss = 0.0657724 (* 1 = 0.0657724 loss)
I0318 15:51:15.351678  3487 solver.cpp:236] Iteration 4800, loss = 0.0904344
I0318 15:51:15.351699  3487 solver.cpp:252]     Train net output #0: accuracy = 0.984375
I0318 15:51:15.351713  3487 solver.cpp:252]     Train net output #1: loss = 0.0904343 (* 1 = 0.0904343 loss)
I0318 15:51:15.351727  3487 sgd_solver.cpp:106] Iteration 4800, lr = 0.01
I0318 15:51:18.285661  3487 solver.cpp:461] Snapshotting to binary proto file _iter_5000.caffemodel
I0318 15:51:18.297734  3487 sgd_solver.cpp:269] Snapshotting solver state to binary proto file _iter_5000.solverstate
I0318 15:51:18.308661  3487 solver.cpp:320] Iteration 5000, loss = 0.0756349
I0318 15:51:18.308693  3487 solver.cpp:340] Iteration 5000, Testing net (#0)
I0318 15:51:19.444846  3487 solver.cpp:408]     Test net output #0: accuracy = 0.9814
I0318 15:51:19.444896  3487 solver.cpp:408]     Test net output #1: loss = 0.0615246 (* 1 = 0.0615246 loss)
I0318 15:51:19.444908  3487 solver.cpp:325] Optimization Done.
I0318 15:51:19.444916  3487 caffe.cpp:215] Optimization Done.

見てください。 net output が #0 と #1 に増えています。 accuracy が正答率で、loss が誤差関数の値です。
最初のテスト正答率は 0.0753 、つまり 7.53% しかなかったことがわかります。つまり、ほぼ当てずっぽうです。
しかしながら、5,000バッチ学習した後には、なんとテスト正答率は 98% にもなっていることが分かります。これは大変大きな進歩と言えます。
これなら「私のネットワークは手書き文字を98%識別できる」と他人に自慢しても分かってもらえるでしょう。

今回のまとめ

 今回は、訓練誤差とテスト誤差について説明した上で、それぞれの正答率の出し方についても紹介しました。
そして、Caffeにて用意されている実行ファイルを使い、訓練する方法を説明しました。
次は、訓練により出来上がった caffemodel を使って、実際の一般データを識別させる方法、つまり、deploy.prototxtの書き方とPyCaffeでの使い方について見ていきます。

Caffeを使ったCNNによる手書き文字分類 [3] – prototxtの作成

 前回は、CNNの構造、およびCNNを作る際に設定しなければならない項目(これらをハイパーパラメータと呼びます)についてひと通り説明しました。
今回は、実際に手書き文字認識を行うCNNを、Caffeが読み込める形(prototxt)で作成します。

prototxtとは

 プロトテキスト(prototxt)とは、ニューラルネットの構造を記述するためのCaffe独自のテキスト形式です。
また、prototxt自体、Googleが開発したデータ構造を記述するためのテキスト形式の一種であるProtobufの文法に基づいたものです。
実はPyCaffeには、このprototxtをPython上から自動生成する機能が備わっていますが、少し面倒なので今回は手打ちします。

prototxtの基本文法

 prototxtは「key: value」という型にしたがってデータの構造を記します。 key の部分は項目名に当たる場所、value はそれに対する実際の値です。
数字の場合は直に数字を打てますが、文字列の場合は “” で囲む必要があります。
例えば、人を表すデータを作りたいとき、年齢(age)と名前(name)を書くとしましょう。この場合は

name: "山田太郎"
age: 99

という風に書けます。もし同じ value に当たる部分に単なる値ではなく複数の key: value の構造を記したい場合は 「key { }」 のように、 {} で囲った中にさらに key: value を書き込んで表現します。
例えば、上の name と age を持つものを person という項目名にしたとします。この場合

person {
name: "山田太郎"
age: 99
}

と表現できます。同時に、この person を2つ並列したい場合は

person {
name: "山田太郎"
age: 99
}
person {
name: "田中花子"
age: 18
}

のように書くことができます。さらに、もし子供がいる場合、同じ name と age を持つ child というものを各 person が自由に持っていいことにします。
その場合、上の “田中花子” だけに子供がいるとすると

person {
name: "山田太郎"
age: 99
}
person {
name: "田中花子"
age: 8
child {
name: "田中太郎"
age: 1
}
}

と書くことができます。基本的にprototxtの文法はこれだけです。実際CNNの構造を書く場合は、上のように、Caffeによってあらかじめ用意されている設定項目を自分で選んで書き足していく形になります。

4つのprototext

 1つのニューラルネットを学習し、一般画像(データセットに含まれない画像)に適用するまでには、4つのprototxt (必ずしも4つのファイルである必要はありませんが、役割としては4種類) を作成する必要があります。
正式に名前が決まっているわけではありませんが、Caffeの各サンプルでの名付けられ方にならい、ここでは以下の4つの名前でそれらを呼ぶことにします。

  • solver.prototxt
  • train.prototxt
  • test.prototxt
  • deploy.prototxt

まずは、この4つのprototxtが何をするものなのかを説明します。
 solver.prototxtは、ニューラルネットを訓練する時に読み込む大元のテキストファイルだと思ってください。ここに書いてある命令に従い、Caffeは各ネットワークの訓練およびテストを行います。
あとで説明しますが、solver.prototxtにはテスト時、訓練時にどのCNNを使うのか、また学習率といった学習そのものの方針を示すパラメータはいくらにするのかを記述します。
 train.prototxtは、訓練時に用いるCNNの構造が記されたテキストファイルで、test.prototxtは、訓練の途中、CNNがどのくらいうまく学習できているのかを調べるため、train.prototxtとは別のデータセットを指定した、同じ構造のCNNを表すテキストファイルです。
CNNの構造自体は一緒なので、節約のため、これらをまとめて1つのprototxtに含めることもできます。今回もそのように、この2つのprototxtに相当するものをまとめてtrain.prototxtに書くという方法を取ります。
 deploy.prototxtは、訓練が終わった後、未知のデータに対して分類を行うためのネットワークの構造を示したもの、要するに本番用のprototxtです。
そのへんのインターネットから拾ってきた画像でも識別できることを友達に自慢したいときに使います。deploy.prototxtはtrain.prototxtからコピペして作ります。
なので、訓練段階では別に作る必要はありません。

train.prototext / test.prototxt の作成

 0から手打ちすることもできますが、余程のことがない限りは適当なサンプルから必要に応じて組み替えていくのがよいでしょう。
以下は、手書き文字を識別するため、最小限の構成で作られたtrain.prototxtの例です。このファイルを、第一回で作った mycnn のフォルダ、つまり lmdb があるところと同じフォルダに保存してください。
この train.prototxt は、上で紹介した train と test の役割をひとつの prototxt で同時に担っています。

train.prototxt

長くて難しく見えるかもしれませんが、図にすると以下のような単純な構造です。

これは、前回まで見てきた、ごく単純なCNNに過ぎないことが理解できます。畳み込み層とプーリング層による特徴抽出は2回行われ、全結合層は2層からなります。
2つめの全結合層は出力層であり、手書き文字の「0」から「9」まで、合計10個を分類したいので10個にする必要があります。
 この構造そのものや、書いてあるパラメータの意味が分からない場合、前の記事で全部紹介しているのでそちらを見てください。
なお、例えば畳み込み層の20枚や50枚といった数字は、電気回路などとは違って、何らかの理論により求めた値ではありません。そのため、なぜこの数なのかを探るのは、実用的な意味においてはほとんど無意味と言えます。
今後訓練できるようになったら、自分で数字を変えて、色々試してみるべきです。
 ただし、数字を変える上で2つ注意点があります。1つは何度も書いているように、メモリは無制限ではないので、これを2000とか50000にすることはできないということです。
実際に大きな値にすれば分かりますが、たいがいメモリ不足でエラーが起きます。
また、本当に10個の数字を識別するのにそのような何万個種類の特徴量が必要なのかについても考える必要があります。要するに、無駄に多い、冗長な数字に設定すると、あらゆる面で損をします。
特に全結合層に関しては、数が多すぎると過学習を起こしやすいことが知られているので、無駄に10000や50000といった数にするべきではありません。
これも別にルールとして決まっているわけではありませんが、全結合層に入る前の出力の数と似たような数を設定しておくとスタートポイントとして無難です。
例えば上のCNNの場合、入力は28×28で、pool2 を通るときには 4×4 になっています。それが50枚あることになるので、800個の数値が全結合層ip1に渡されます。なので800に近い値にしておくことがよいスタート地点になるでしょう。
電卓を叩いても800は出せますが、Pythonでこの数を一瞬で調べられる方法があるので、それについては後で紹介します。

 もう1つは、出力サイズです。前回紹介したように、畳み込み層ではサイズ $n$ に対して周囲が $(n-1)/2$ 、だけ縮み、プーリング層ではストライド $s$ のとき約 $1/s$ 倍画像が縮みます。
つまり、小さい画像に畳み込みやプーリングを使いすぎると途中の知らない間に画像が $1 \times 1$ まで縮んでしまい、本当にそれ以上畳み込みやプーリングをやる必要があるのあか考える必要が出てくるような状態に陥ります。
確かにサイズ1の畳み込みといったテクニックは存在しますが、意図せず事実上そのような処理になってしまったことと、意図的にそうなるよう設定したのでは事情が違います。
 以下、構造について確認したところで、各層のprototxtの記述方法について紹介します。

各層の基本的な記述方法

まず、train.prototxt または text.prototxt では、以下のようにCNNの構造が記されます。

name: ""
layer{
name: ""
type: ""
bottom: ""
top: ""
}
layer{
name: ""
type: ""
bottom: ""
top: ""
}
...

 最初に name でニューラルネット全体に名前を付けます。
出力層まわりの構造やパラメータなどが違いますが、上の図のような構造で、同じMNISTの文字分類を行う目的を持ったニューラルネットはY.LeCunらによる “LeNet” という名前のネットワークによってはじめて提案されたため、今回はその名前を持ってきています。
 あとは、入力データ、畳み込み層、プーリング層、全結合層、誤差(後で説明)など、全て上にあるように layer で囲まれた構造を並べて構成します。
 layer の構成方法ですが、これは自分が使いたい層によって微妙に違います。しかしながら、以下に記すことはどのような層であっても同様です。
まず最初に name により層の名前を付けます。これも決まりはありませんが、慣例(一般的な論文や、Caffeのサンプル)では以下のように付けられています。

  • $k$ 回目の畳み込み層: conv$k$
  • $k$ 回目のプーリング層: pool$k$
  • $k$ 回目の全結合層: fc$k$ または ip$k$

畳み込み層は英語で convolutional layer なので conv, プーリング層は pooling layer なので pool 、全結合層は fully connected layer なので fc です。
普通は全結合層と呼びますが、この部分は数学でいうと前回書いたように「内積」(inner product)という計算に相当するため、Caffeでは inner product layer と呼ばれており、ip と書かれることもあります。
ここではCaffeの各サンプルにならって ip の規則で名付けることにします。
 type は重要で、Caffeによってあらかじめ定義された値の種類の中から、必要な層の名前を選んでここに書きます。Caffeの Layer Catalogue というページに全種類載っていますが、ここでも今回使うものを掲載します。
また、make する前のソースコードを自分でいじると、新しい層を作ることもできます。
なお、一般的なCNNの構造を説明する上では「層」ではないものも、Caffeでは便宜上 layer として定義する必要があるので、混乱のないようにしましょう。

  • “Convolution” : 畳み込み層
  • “Pooling” : プーリング層
  • “InnerProduct” : 全結合層
  • “ReLU” : 活性化関数にReLUを適用 (後で説明)
  • “Data” : 入力層 (後で説明)
  • “SoftmaxWithLoss” : ソフトマックス関数により交差エントロピー誤差を算出 (後で説明)

 結局どれも重要ですが、一番注意したいのは top と bottom についてです。これは、上のCNNの構造を示した図でいう各層を結んだ「→」に相当する関係を規定しています。
要するに、Caffeではたくさんの layer を作っておいて、それぞれ bottom と top によってそれらを連結することで CNN を作ります。
 top は、この層の出力をどのような名前で伝達するかを指定します。これは name とは別で、実際プログラミングをする上で使うのはこちらの方の名前です。
例えば top に “input” という名前をつけると、その層の出力データは “input” という名前であとで Python からアクセスできるようになることを意味します。
基本的には conv1 や pool2 といった、 name と同じものにしておけばよいでしょう。
 bottom は、そうして top により生成されたデータのうち、この層はどれを受け取るのかを指定するものです。
上の例でいう “input” を別のある層で受け取りたい場合には、 bottom: “input” と書けばよいですね。
 最後に、train.prototxt と test.prototxt を1つにまとめたい場合の方法についてです。通常これらは別々に書いても問題ありません。
なぜ同じ構造のはずなのに別々に書く必要があるかですが、それぞれデータセットが違っているからです。
train では訓練のためのデータセット、test では、訓練と同じものを使うと訓練用データにしか適応できない(過学習)おそれがあるのでそれとは別のデータセットを通常指定します。
なので、同じ構造であっても、訓練用データのLMDBを指定したCNNと、テストデータのLMDBを指定したCNNで別々に用意する必要があります。
 1つにまとめたい場合は、上でダウンロードした train.prototxt にもあるように、以下のように書きます。

name: "LeNet"
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
scale: 0.00390625
}
data_param {
source: "mnist_train_lmdb"
batch_size: 64
backend: LMDB
}
}

説明していないものもいくつかありますが、それは気にしないでください。この時の「include」の部分が重要です。
訓練時だけ存在するようにしたい層は、上のように phase: TRAIN を含んだ include を書くと実現できます。
同様に、訓練時ではなく、現時点での訓練データではないデータ、つまりテストデータに対する正答率を見ている時にだけ存在させたい層は phase: TEST を含んだ include を書けばよいのです。
この記述を駆使することで、基本的な構造は流用しながら、訓練時、テスト時だけ一部の層が異なっているニューラルネットワークを作ることができます。
 solver.prototxt だけは上で書いたルールとはまた違うので別途説明しますが、以下、各層ごとに基本的な書き方を見ていきます。

入力層(Data)

 実際の入力データを供給する層が Data です。 type: “Data” と書くことでこの層にできます。まずは、 train.prototxt の中にあるこの層の記述について見てみましょう。

layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
scale: 0.00390625
}
data_param {
source: "mnist_train_lmdb"
batch_size: 64
backend: LMDB
}
}
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST
}
transform_param {
scale: 0.00390625
}
data_param {
source: "mnist_test_lmdb"
batch_size: 100
backend: LMDB
}
}

先ほど説明したように、訓練時とテスト時では違うLMDBを読み込まなければならないので、includeで訓練用とテスト用の Data 層をうまく分けています。
当然この層は入力層なので、他の層から受け取るものはありません。したがって入力層に限っては bottom が必要ありません。
一方で、この層から生成されるデータは2つあります。LMDBには画像とあわせ、その画像の正解ラベルが何であるかという情報が格納されているからです。
正解ラベルは、識別したい画像の種類のうち、最初から数えて何番目のカテゴリなのかを数字で指定します。この数字はプログラミングなどではおなじみですが、1ではなく0から始まります。
今回のMNISTの手書き文字の場合、0〜9とちょうど0からはじまっているため、数字 = 正解ラベルにすれば分かりやすいでしょう。なので今回は、数字がそのまま正解ラベルに対応するようにします。
例えば犬猫人を判別したい場合は、自分で犬=0, 猫=1, 人=2 と定義した上で、LMDBを作る際も、この画像は犬なので 0、これは人なので 2 ・・・といったラベルを手動で付ける必要があります。
1つめの top では、画像データを受け取ります。2つめの top で、正解ラベルを受け取っています。これらはどういう名前でも良いですが、分かりやすいのでそれぞれ top: “data” と top: “label” と名づけました。
 transoform_param の部分は、データの値を改変したい場合に使います。
MNISTのLMDBから直接受け取った画像データ(に限らず普通の画像データ)は、0〜255までの256段階で画素値が記録されていますが、Caffeはこれを0〜1の範囲で使うため、単純に $1/256$ に相当する 0.0039 という値がここで指定されています。
この scale で指定した倍率だけ入力データの画素値が一律スケーリングされます。つまり、この transform_param を書いたことで、0〜255 までの画素値が 0〜1 の範囲に縮められるのです。
通常の画像分類の場合、とりあえずおまじない感覚でよいのでこれをコピペして流用しましょう。
 data_param 各種で、具体的な入力データの場所などを指定します。source は、現在のパスから見たLMDBフォルダの場所です。この場合、LMDBフォルダと同じ場所に train.prototext を置き、パスも同じ場所で訓練を行うという前提のもとでこのような書き方になっています。
もしフォルダが違う場所にある場合は、相対パスなり絶対パスなりで指定する必要があります。
 batch_size は、確率的勾配降下法(SGD)におけるバッチサイズを指定します。今回はとりあえず64にしてありますが、32でも128でも50でも適当な数にしておきましょう。
MNISTの場合、28×28しか画像サイズがなく、ネットワークも小規模なので、メモリをあまり消費しないという考えから64と大きめにしてあります。リッチな環境なら128でも256でも行けるかもしれません。
ただし、この数が大きすぎると、本当の最小値ではないところで解が落ち着いてしまう状況を偶発的に抜け出せるというSGDの良さが失われることになるため、バッチ数の大きさとSGDの恩恵は天秤にかけるようにしてください。
 backend は、 source で指定したデータベースが何の種類なのかを指定します。普通は LMDB なので LMDB と書いておけば問題ありません。なお面倒なことに、ここだけ “LMDB” のように “” で囲んではいけないので注意しましょう。
 説明のため色々書きましたが、結局画像分類をする時は source と batch_size くらいしかいじる場所がないので、一度動かすことができれば後はコピペ流用で問題ないかと思います。

畳み込み層(Convolution)

 畳み込み層は type: “Convolution” と書くことで設定できます。 bottom でこの層が受け取るデータを、top で処理後のデータの名前を指定します。
以下が train.prototxt の中にも書いてある、典型的な書き方です。

layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
convolution_param {
num_output: 20
kernel_size: 5
weight_filler {
type: "xavier"
}
}
}

convolution_param の中で前回説明した畳み込み層のパラメータを指定していきます。num_output がフィルタの枚数、 kernel_size がフィルタのサイズです。
 weight_filler というのは名前の通り「重みの初期化方法」です。これは特にこだわりがなければ type: “xavier” をコピペして使いまわして下さい。
ニューラルネットにおいて、各パラメータは自動更新されるとしましたが、このような手法の場合、まずはパラメータを全部ランダムにしておくことが考えられます。
そのランダムの仕方の中でも、フィルターの枚数やユニットの個数に応じてランダムの幅を調節してくれるアルゴリズムがこの type: “xavier” です。
なので、多くの場合は weight_filler はこの type: “xavier” で放置しておいて問題ありません。
例えば、ランダムで初期化しない初期化法の1つとしては、「他のタスクで学習したニューラルネットのパラメータを流用して初期値にする」といったものがあります。

プーリング層(Pooling)

 プーリング層は type: “Pooling” と書くことで設定できます。 bottom でこの層が受け取るデータを、top で処理後のデータの名前を指定します。
以下が train.prototxt の中にも書いてある、典型的な書き方です。

layer {
name: "pool1"
type: "Pooling"
bottom: "conv1"
top: "pool1"
pooling_param {
pool: MAX
kernel_size: 2
stride: 2
}
}

pooling_param の中でパラメータを指定します。前回説明したように、プーリングにはいくらか種類があるので、 pool で指定できるようになっています。
これも “” で囲まない点に注意が必要ですが、MAXプーリングは pool: MAX で指定できます。プーリングの枠の大きさは kernel_size で、ストライドは stride で指定できます。
 枚数の指定場所がないことに注意してください。なぜならば、プーリング層は畳み込み層に一律処理をかける層であるため、畳み込み層の num_output と同じ枚数の出力が出てくるからです。

全結合層(InnerProduct)

 全結合層は type: “InnerProduct” と書くことで設定できます。 bottom でこの層が受け取るデータを、top で処理後のデータの名前を指定します。
以下が train.prototxt の中にも書いてある、典型的な書き方です。

layer {
name: "ip1"
type: "InnerProduct"
bottom: "pool2"
top: "ip1"
inner_product_param {
num_output: 500
weight_filler {
type: "xavier"
}
}
}

毎回名前が微妙に変わってややこしいですが、一度動けば基本コピペ改変で済むので我慢しましょう。 inner_product_param でパラメータの詳細を指定します。
 num_output でこの層のユニットの数を、weight_filler で初期化方法を指定しています。なお、最後の前結合層、つまり出力層の num_output は、分類したいカテゴリの数と同じにする必要があります。
今回の場合は 10 です。
 この層は名前の通り内積計算しかしない、つまり活性化関数の適用は含まれないので、後述の方法で活性化関数の層(Caffeの仕組み上、便宜的に層に含めることにする)と連結させる必要があります。

活性化関数(ReLU)

 本来であれば活性化関数は層には数えませんが、Caffeの構造上 layer として定義する必要があります。
要するに InnerProduct 層は文字通り内積(掛け算足し算)しかせず、その後活性化関数を通したいなら自分で定義して通しなさいということです。

layer {
name: "relu1"
type: "ReLU"
bottom: "ip1"
top: "ip1"
}

書き方が特殊ですが、bottom と top を同じにして、 type: “ReLU” とすることで ReLU 関数を活性化関数として指定することができます。
これにより、 ip1 の出力は内積をとった後の値ではなく、 ReLU を適用した後の値に修正されます。

誤差関数(SoftmaxWithLoss)

 これも層ではありませんが、やはりCaffeの仕組み上 layer として定義する必要があります。典型的な書き方は以下のとおりです。

layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip2"
bottom: "label"
top: "loss"
}

bottom は2つあります。1つめの bottom は、ネットワークの出力、2つめはそれと比較する正解データです。この場合は最初に Data 層から読み込んだラベルなのでその時名づけた label を指定しています。
 こう書くことで、自動で ip2 の10個の出力をソフトマックス関数により確率化し、正解ラベルとの違いを(交差エントロピー誤差関数という形で)計算して数値化してくれます。
ここで計算した正解ラベルとの違いの度合いをもとに、ニューラルネットはパラメータの更新を自分でかけます。
 前回少しだけ紹介したように、本来は勾配という微分計算を全パラメータに対して行う必要があるところ、バックプロパゲーション(誤差逆伝搬法)というテクニックにより、出力層側から入力層側に向かって単純計算だけでパラメータの更新を行います。
簡単に言うと、実はそれぞれのパラメータの微分は独立なものではなく、ある層 $l$ 手前 $(l-1)$ 層のユニットのパラメータに関する誤差関数の微分は、 $l$ 層のユニットのパラメータの微分が求まっていると、微分計算をしなくても自動的に単純計算で決まる、つまり微分を微分でないものの計算に置き換えられるということを、前回紹介したニュールネットワークの構造に照らし合わせて数学的に示したものです。
そしてその $l$ 層のパラメータは $(l+1)$ 層から、この層もまた $(l+2)$ 層から・・・という具合になるので、結果的には最終層のパラメータの微分が分かっていれば全部単純計算で済むことになり、しかもこの最終層も最終的には微分は不要(数学的に微分できて、その結果が微分を含まない形にできる)であるため、出力層側から値を決めて前方に向けて逆方向に伝搬されるということです。
以上の説明でなんとなく分かった気になれる方は、その辺の参考書で数式を勉強してください。実用上はデータの伝搬は入力→出力、パラメータの更新は出力方向→入力方向とさえ知識として知っておけば十分です。

solver.prototxtの書き方

 これで、訓練用のデータセットを使うCNN、およびテスト用のデータセットを使う同じ構造のCNNをひとつのファイル train.prototxt の中に書くことに成功しました。
あとは、このCNNを使って実際に訓練するための方針を示したファイルである solver.prototxt を別途作る必要があります。
 これも、最小限の量に抑えた solver.prototxt を以下に用意したのでダウンロードして LMDB や先ほどの train.prototxt と同じ場所に保存してください。

solver.prototxt

これがその中身です。

net: "train.prototxt"
base_lr: 0.01
lr_policy: "fixed"
max_iter: 5000
display: 100
test_interval: 200
test_iter: 100
solver_mode: GPU

 net で訓練する時、どの prototxt にするのかを指定します。今回のように訓練用、テスト用を同じファイルに書いた場合は net: “ファイル名” で良いですが、include を使わずに別々のファイルにした場合は次のように書いてください。

train_net: "訓練用prototxt"
test_net: "テスト用prototxt"

 base_lr は、学習率を指定します。これは適当に小さい値であればいいので最初は 0.01 や 0.001 くらいにして、あまりに繰り返しても進捗が見られない場合は10倍くらい、逆に誤差が爆発(莫大な値に張り付いて減らなくなってしまった)場合は1/10にしてみるなど試行錯誤しながら決めましょう。
無駄に小さいと、例えば本来であれば30分でできる学習内容なのに、3時間、ヘタしたら3日もかけてしまうことを意味します。
 lr_policy も必須の設定項目で、学習が進むに連れて、学習率をどうするかここで方針を指定します。ずっと変えない場合は lr_policy: “fixed” にします。
今回はそのようにしてありますが、一般には学習が進むに連れて小さくし、微調整をかけるという方法が取られます。
 max_iter は、何バッチで訓練を打ち切るか設定します。今回は5,000にしてありますが、少なすぎる場合、学習の成果が出る前に打ち切ってしまう可能性があり得るため、大きな値にしておき、適度な間隔でスナップショットを撮影しておくというのでもよいでしょう。
スナップショットの作り方などは、後の回で紹介します。
 display は、何バッチおきに訓練中のネットワークの誤差を報告するのかを設定します。ここでは 100 なので、100バッチ進むごとに、ネットワークの誤差が報告されます。
 
 test_interval と test_iter はテストに関する方針を記載します。 test_interval は、何バッチ分訓練を進めたらテストを実施するかを記録します。
1の場合は1バッチ進むたびテストが行われますが、テストも全く計算コストなしに行えるものではないため、普通は何十回や何百回といった間隔が設定されます。
また、SGDで学習するため、細かくテストすることにはそれほど大きな意味はありません。なぜなら、短期的に見れば、誤差が激しく上下することがあり得るからです。
そのように上下するのはSGDを使う以上わかりきったことであり、あくまで重要なのは長期的に見た場合、一貫して誤差が下がる傾向が見られることなので、処理時間を無駄に増やさないためにも、少しは間を空けておきましょう。
ここでは200バッチ毎にテストをする設定にしてあります。
 test_iter では、テスト時には何バッチ分テストを実施するのかを指定します。これは、テスト用ネットワークの prototxt に記載したバッチ数で、ここに書いた回数だけテストが行われることを意味します。
今回はバッチサイズ100、 test_iter が 100 なので、合計10,000枚のテストを実施します。これはちょうどMNISTのテスト用データセットの全枚数と一緒です。
このように、 $ {\rm batch\_size} \times {\rm test\_iter}$ がデータセットの枚数を上回らないよう注意しましょう。
 
 最後に solver_mode で CUDA による計算をするのか CPU で計算するのか指定します。GeForce GTX各種を使っている場合で、GPUモードで make した場合はこのまま GPU に、何らかの事情で CPU で学習したい場合は CPU にしましょう。

今回のまとめ

 今回は、prototxtの文法および、必要になる prototxt の種類について説明しました。
訓練用とテスト用のニューラルネットは別々に定義しなければならないものの、構造が共通なので多くの部分を流用する場合はひとつの prototxt に書いてもよいことも説明しました。
その後、solver.prototxt の書き方について紹介しました。
 これで学習の準備が整ったので、次回は実際に Caffe で学習を行う方法について説明します。

Caffeを使ったCNNによる手書き文字分類 [2] – CNNの構造と学習の仕組み

前回、ディープラーニングにおけるデータセットの重要性について説明しました。
さらに、CNNが識別器として動作するためには、訓練という過程が必要であり、それには多数の訓練データとその正解データが必要であることを述べました。
加えて、実際の未知画像に対してもどれくらい適応可能なのかを知るために、正答率の目安を知るためのデータ、つまりテストデータとその正解データも必要なことを説明しました。
 ここでの目的は、手書き文字の分類をMNISTというデータセットを使い行うことですが、実際にこのデータセットを使って学習をさせるには、CNNの構造を定義する必要があります。
そこでこの記事では、CNNの構造を簡単に説明します。もし動くコードが先に欲しくて、中身がどうなっているかは後回しで構わない、あるいは興味がない人は、この記事は飛ばして次に行っても構いません。
 おそらく他のインターネット記事に多くあるように、単に動くコードを説明するだけであれば、この記事にあるような動作の説明はいらないと思います。
しかしながら、CNNの分野においては、理論的というよりは試行錯誤的、半ば職人芸的に構造やパラメータを決定しているのが現状です。
つまり、もしうまくいかなかった場合、一体どのパラメータがどういう意味なのかを知らなければ何の方針も立たず途方に暮れることになってしまいます。
この記事では、そういった事情も踏まえ、できる限り数式で議論することは避けながらも、それぞれの要素の動作、および各パラメータについての紹介も行います。

CNNの構造

典型的なCNNの構造を以下の図に示します。

何も知らないと、いきなりこれを見て怖気づきそう(特に右の方の○に大量の線が引いてある部分)ですが、気にする必要はありません。
結局上のような構造を生成の上、計算くれるのはCaffeであり、あなたはこの恐ろしく複雑に見える処理を自分で書く必要がないのです。
 一番簡単にCNNの構造を分けるのであれば、青色の枠線で囲まれた2ブロックです。
この枠線の中身にある変な物体たち、特に○の集まりは気になるでしょうが、実用上詳しい仕組みを説明するのは後回しでも構いません。
CNNに限らず、一番重要なのはそのシステムでは何が入力され、何が得られるのかであり、まずはその点を押さえましょう。
 例として、前回から使っている「人」「犬」「猫」の判別をしたいのだとしましょう。上の画像もそれに応じた作りになっているのですが、まず入力は何らかの物体が写っている写真です。
上の画像では人が写っています。CNNはこのように画像を直接入力として受け付けます。ただし、仕組み上画像サイズは一定でなければならないので注意が必要です。
しかしながら、それは実用上の些細な問題であり、もし入力する画像サイズが一定でないのなら、単純に全ての画像を拡大縮小してCNNに入力すればよいだけのことです。
あるいは、大きな画像の中から、判定したい必要な場所だけを切り抜くということも考えられるかもしれません。
このように、CNNの入力にふさわしい形に画像を加工する作業のことを正規化と呼びます。CNNは一定サイズに正規化された画像を受け取ります。
 画像を受け取ると、CNNは内部にあるパラメータに沿って特定の計算を次々と行います。最初のブロックは、いわば「特徴抽出部」といえるもので、
その画像が人なのか、犬なのか、あるいは猫なのかを判定するために必要な視覚的情報を入力画像から抽出するという役割を持ちます。具体的に何をしているかは少しだけあとで紹介します。
 一方で、後ろの○が並んでいる奇妙な部分はいわば「識別部」といえるものであり、抽出された情報をもとに、この情報は一体人、犬、猫のどれにあたるのかを所定の計算によって判断、つまりパターンを分類します。
その結果、判定結果、つまり出力は「確率」という形で出てきます。これは、全ての用意されたカテゴリについて確率が出てきて、合計すると100%になるような仕組みです。
例えば上の画像を入力すると、人である確率は80%、犬である確率は10%、猫である確率は10%といった具合です。これを受けて、確率が最も高い「人間」を答えであるとします。
これがCNNによるクラス分類の大まかな流れです。

この項のまとめ

  • CNNは画像を入力として受け取る
  • CNNに与える画像サイズは一定でなければならず、そのサイズに一致しない場合は正規化が必要である
  • 画像を受け取ると、判断に必要な情報の抽出作業が行われ、その後その情報をもとに識別作業が行われる
  • CNNの出力は、入力した画像が用意した出力カテゴリのうちどれであるのか「確率」という形で算出される

特徴抽出部

 特徴抽出部は、先ほど紹介したように、入力された画像から、判断するために必要な情報(特徴量)を抽出します。
 特徴量という言葉については難しく考える必要はなく、例えばトマトときゅうりを判別したいとき、丸さや赤さといったものを画像から数値化しても、それは特徴量であるといえます。
従来の方法では、この特徴量というものに何を採用すればよいかあらかじめ規定しておき、それを取り出すための方法を人間が手作りで考える必要がありましたが、例えば今例にした赤さとか丸さというのは根拠もなく直感的に考えただけの指標であり、しかもそれらを具体的にどうコンピュータにわかる形で算出すればいいかすらも自明ではありません。
つまり、特徴量の抽出は思うほど簡単なことではありません。
 しかし、CNNは、訓練される過程で、適切な特徴量を自動で獲得できるよう自分でパラメータを変更していきます。
人間が手作りしたわけではないので、結果抽出された特徴量に赤さや丸さといった情報があるのかについては明らかではないものの、CNNは目的の物体をうまく判別できるような形で情報を抽出できるのです。
 特徴抽出部は、具体的には、畳み込み層、プーリング層というものが何回か連なることにより構成されています。
畳み込み層ではその名前の通りの「畳み込み」と呼ばれる処理を行い、情報を抽出します。
一方でその後のプーリング層は、畳み込み層で得られた情報を要約するとともに、位置に対する不変性、つまり、多少物体が周囲に移動していても、同じ物体だと識別できる能力を強める役割があります。
必ずそうしなければならないルールはどこにもありませんが、典型的なCNNでは、畳み込み層とプーリング層をひとつのペアとして、それを何回か繰り返して特徴抽出を行います。
その結果、この繰り返しをすればするほど、出力層に近づくにつれ、色や形といった明らかな視覚的情報ではなく、より抽象的な情報を抽出されるようになります。
ただし、層を追加すると、より多くのメモリを必要とするので、層は無制限に追加してよいわけではなく、メモリの制限とも相談しながら層の深さや層のパラメータを決定する必要があります。
あまりに層の数を欲張ったネットワークにしてしまうと、例えば入力がたった128×128というサイズしかないのに GeForce GTX TITAN X (12GB) でもメモリ不足といった事態に簡単に陥ります。

この項のまとめ

  • 特徴抽出部では、画像から識別に必要な特徴量を抽出する
  • 畳み込み層とプーリング層から構成されており、これらを幾重にも重ねあわせることで構成される
  • 構造が深くなるほど一般に抽象的、より高レベルな情報を抽出できるが、好きなだけ深くすることはできず、メモリ制限と相談する必要がある

畳み込みフィルタ

 先ほど、特徴抽出部には畳み込み層とプーリング層の2種類があり、基本的には「畳み込み層→プーリング層」を1つの組として、これが何回か繰り返されると説明しました。
次に、この2つの層はどのように違っているのか、つまりどんな感じの処理をしているのかを、数式を極力出さないという方針上、非常に粗い説明にはなりますが、見てみましょう。
 畳み込み層が行っているのは、画像処理の世界でいう「畳み込みフィルタ」という処理に大変近い処理ですが、OpenCVで遊んだことがない人にとっては畳み込みという言葉もフィルタという言葉にも馴染みがないかと思います。
 フィルタとは、文字通り情報のふるい分けを行うことであり、ある情報から不要な情報を取り除いて必要な情報を残すことを言います。逆に言えば欲しい情報を抽出するということです。
特に、「畳み込み」という計算で画像全体を処理していくタイプのフィルタは「畳み込みフィルタ」と呼ばれています。今後、単に畳み込みフィルタを「フィルタ」と呼びます。
 典型的なフィルタ($3 \times 3$サイズのフィルタ)を以下に示します。

それぞれのマスは、画像でいう1ピクセルに相当するものと思ってください。つまり、3ピクセルの幅と3ピクセルの高さを持つフィルタが上の図です。
縦横サイズは違っていても計算の定義上何ら問題ありませんが、扱いやすいことから、よほどの理由がない限り形は正方形です。
そのため、今後この記事でも単にサイズ $n$ のフィルタといえば、$n \times n$ ピクセルの大きさを持つフィルタのことを指すものとします。
 上の数字は、マイナスでもプラスでも自由な値が入ります。この値に沿って、所定の方法で計算(畳み込み)を実施すると、フィルタの出力が得られます。
このとき、上のパラメータをうまく選ぶと、特定の機能をもたせることができることが知られています。
特にその中でも重要な機能を持つ有名なものには名前がついていて、例えば上のフィルタは「横方向微分フィルタ」という名前がついており、
横方向のエッジ検出する機能(画素の濃淡の差が大きい場所がどこかを強調する役割)を持っています。

img.png

例えばこれが元の画像であるとき、背景は白なので、フォルダアイコンの境界線や文字のあたりがエッジに相当する場所だろうという見当がつきます。

out.png

これが実際の横方向微分の実行結果です。確かに、色の変化が著しい場所のみが白く浮き上がっている様子が理解できます。
横方向なので、その一方で縦方向のエッジにはあまり反応しないことも理解できます。もちろん、この逆パターンの縦方向フィルタもあります。
 なお、上の結果は上で示したフィルタではなく横方向のソーベルフィルタというものを使っていますが、パラメータが違うだけで原理は全く一緒です。
これはOpenCVの手助けを借りた以下のコードで簡単に同じことができます。縦方向にしたい場合は cv2.Sobel の「1,0」を「0,1」にすればいいだけです。

import cv2
img = cv2.imread("img.png",0)
out = cv2.Sobel(img, cv2.CV_64F,1,0,ksize=3)
cv2.imwrite("out.png", out)

“img.png”と”out.png”は、自分で適用したいファイルの場所、保存先のファイルの場所へそれぞれ書き換えましょう。
これで、フィルタは縦、横一定のサイズを持った数字の集まりであり、数字の組み合わせ方によって画像から特定の特徴量が抽出できることもなんとなく分かったかと思います。
CNNは、上に書いたようなフィルタのそれぞれのマスの数字が更新可能なパラメータになっていて、訓練される過程で自ずと分類に必要な特徴量が抽出できるよう、このパラメータを更新していくのです。
 次に畳み込みの計算についてですが、簡単なイメージだけつかんでおきましょう。

乱暴に書けばこのような感じです。先ほどのフィルタは赤い枠にしてあります。このフィルタが画像左端から右端までをスキャン(走査)します。
それが終わると、次は1ピクセル下にずれて同じことをやります。これを、一番下に達するまで繰り返します。
なお、上の画像についてですが、本当はフィルタの1マスは画像の1ピクセルに相当するものです。上のものは明らかに画像の1ピクセルと対応しておらず、厳密には正しくないので注意してください。
ただ、イメージ自体は十分に理解できるかと思います。
 以下の例でもう少しわかりやすくイメージを掴んでみましょう。ご存知の通り、画像は色の三原色、赤、緑、青の3つのチャンネルから成り立っています。
色の情報がない、単に光の濃淡のみが記録された画像、つまり世間一般にいう白黒画像はグレイスケール画像と呼ばれており、以下はグレイスケール画像に対してフィルタを適用する場合を考えます。
(が、カラー画像もグレイスケールの1チャンネルに対して、チャンネル数が3に増えるだけで考え方は一緒です)

最初に、フィルタは画像の一番左上の $3 \times 3$ マスの領域に作用します。画像の対応する場所にフィルタを載せてみましょう。

このようになるでしょう。見難くなるので、フィルタの値は右下に小さく書くようにしました。
次に、それぞれのマスにある値同士を掛け算します。

計算結果は矢印で右に示しました。
最後に、全部の値を足します。すると3とマイナス3が同じ数あって、残りは0なので0になることがわかるでしょう。
このときの計算結果「0」が、一番左上の場所に対するフィルタの計算結果です。なので、処理後の一番左上の画素値は「0」であることがわかりました。
このように、決まった並びの数字の列と、もう1つの同じ並びを持つ別の数字の列において、対応する要素同士をかけた結果を足し合わせたものを内積といいます。
 次に結果を1つずつ記録していきましょう。なお、図でわかるように、フィルタを画像の中に収められる範囲で計算をすると、上下左右とも1マスずつ小さくなります。実用上はここが一番重要な知識です。
フィルタの処理結果は全部で9個得られることが事前に分かるので、$9\times 9$のマスを作っておきましょう。

次も同じように計算すると、赤く?になっている場所は$-6$であることが確かめられます。あとはこの手順に沿って、もうひとつ右にフィルタをずらし、次に下に1マス降りて・・・とすれば、フィルタによる処理が完了します。
このような一連の計算のことを畳み込みといいます。
数式で書くと非常にわかりにくいのですが、計算の原理はこのように単純で、「1点の値を決めるのに周りの値を使う」のが畳み込みの特徴です。
現在見ている場所の処理結果を決定するために、$3 \times 3$ といった局所的な領域を考慮していきます。
 これをCNNの仕組みの中で見てみると、最初は局所的な領域(特定の方向のエッジなど、簡単な色形の情報)を見ながら判断をするわけですが、畳み込み層とプーリング層を通っていくうちに、
次第に見る領域の範囲が(画像が縮むので相対的に)大きくなっていき、判断される情報も単純な形などではない、より抽象的なものになっていくのです。
なお、フィルタのサイズが大きいと、より広い領域を見ながら判断できるようになりますが、見ての通り$3 \times 3$のフィルタの時点でたった1点を決定するのに9回も掛け算をしてそれを全部足し算しています。
なので、計算コストの問題やメモリのから、CNNにおいてはむやみにフィルタのサイズは大きくできません。
これも決まりではないですが、一般的なCNNの場合、最初は大きめのフィルタにして、奥の層に行くほどサイズを縮めていくのが一般的です。
 最後に、CNNの実用上重要な部分についてです。上の具体的な計算方法は忘れても問題ありませんが、知識として次の2つは知っておいた方がよいでしょう。

  • フィルタのサイズは基本的に奇数×奇数
  • 畳み込みの結果、フィルタのサイズが$n$のとき、$(n-1)/2$だけ周囲が小さくなる

 1つめは対称性によるものです。もしサイズが奇数なら、上の図で見てきたようにぴったり周囲平等に見ることができますが、偶数だと上下左右どこか必ずほかよりも見渡せないところが出てきます。
この理由から、特別な理由がない限りはフィルタのサイズは奇数(3,5,7,9,…)にしておきましょう。これはCNNに限った話ではなく、普通の画像処理においても同じことです。
 2つめはフィルタサイズが奇数ならという話ですが、処理後の画像サイズは $(n-1)/2$ だけ周囲が小さくなります。
本当はフィルタの中心を画像の左上に合わせ、画像の右下がフィルタの中心になるまで同じことをやれば処理後もサイズは不変ですが、こうすると面倒なことがおきます。
それは、画像をはみ出た場所の画素値がないということです。適当な値で処理して埋める方法(パディング)もありますが、このことから、通常はスタート地点をフィルタがちょうど左上に収まる場所からにします。
つまり、サイズが3の時は、中心から見て上下左右1マス分、5の時は2マス分、7の時は3マス分処理できない点が出てくるということを意味します。このことから、式で書けば画像は $(n-1)/2$ だけ小さくなるのです。

畳み込み層

 以上を踏まえた上で畳み込み層について簡単に説明します。これまで説明したのは一般的な画像処理における畳み込みフィルタの振るまいでしたが、畳み込み層はこれに加えてもう少し手の込んだことをやっています。

これが構造の図ですが、畳み込み層は上の模式図からも分かるように、今見てきたフィルタが何枚も集まって1つの畳み込み層を形成しています。
一般にはその枚数が多いほどより多くの種類の特徴量を抽出できることになりますが、枚数が多すぎると莫大なメモリを消費するので、やはり1000枚2000枚といったフィルタの枚数は気軽には設定できません。
これも決まりではありませんが、一般的には64,128,192,256…といったコンピュータ的にきりのいい枚数に設定されていることが多いようです。もちろん50や100でもよいでしょう。
 実際の畳み込み層の振る舞いについて見ていきます。入力はカラー画像、つまり3チャンネル(3つの縦、横サイズが同一の数字の行列)だとしましょう。これに対する1枚のフィルタの挙動は次のとおりです。

図のように、入力は3つのチャンネルがあるわけですが、先ほど書いた畳み込み処理をそれぞれに適用し、3つの出力が得られます。
 次に、1枚のフィルタに対して得られた3枚の出力を「圧縮」します。どうやるかというと、全部重ねあわせて、対応する点で3枚の値をそれぞれ合計します。

なお、本当はただ和を取るだけではなく、バイアスと呼ばれる変数も付加しますが、ここでは省略します。
 以上から、1枚のフィルタでできるのは1枚の出力であることがわかりました。
言い方を変えれば、入力時点でのチャンネルが100であろうが、100000であろうが、畳み込み層のフィルタの枚数が$k$枚なら、出力も$k$チャンネルになることを意味しています。実用上はこの知識が重要です。

この項のまとめ

  • 畳み込み層は、自分で決めた複数枚のフィルタから構成される
  • $k$枚のフィルタからなる畳み込み層を適用すると、入力のチャンネル数によらず$k$チャンネルの出力が得られる
  • 出力される$k$枚の画像サイズは全て同じであり、フィルタのサイズが$n$の時、$(n-1)/2$だけ周囲が小さくなっている

プーリング層

 プーリング層は、すでにいくらか書いているように、決まった領域の値をひとまとめにし、位置に対する感度を低くするかわり、位置変化に対する認識能力を上げるための層です。
プーリングという処理にはいくつか種類がありますが、多くの場合は以下に説明するMAXプーリングというものが使われます。
 やはり言葉で全部説明するより図があったほうがよいので、図で見てみます。

このような画像に、プーリングを適用したいとします。プーリング層においては、サイズ$n$およびストライド$s$というパラメータが問題になります。
前者はフィルタ同様、局所的な領域を見るわけですが、その時に見る領域の広さを表していて、ストライドは移動するときの幅、つまり粗さを示しています。
例えばストライドが2のときは、1マスではなく2マスずれながら計算が行われるということです。
ここでは、サイズが2でストライドが2だとしましょう。

すると、上の図のようになります。サイズが2なので$2 \times 2$の領域を見て、ストライドが2なので2ずつずれていきます。
MAXプーリングの場合、それぞれの囲まれた領域の中で、最も大きい値をその領域の値にします。
つまり、上の例の場合、「3,3,3,3」という$2 \times 2$の画像が出来上がり、これが出力です。
これを、畳み込み層から得られた枚数全てに対して適用します。そのため、畳み込み層の出力枚数とプーリング層の出力枚数は同じです。
 なお、実用上重要な知識としては、ストライド$s$の分だけ出力サイズが小さくなります。つまり、画像は約$1/s$(「約」と付けたのは、奇数サイズの場合はちょうど半分にはならないため)に縮小します。
また、見ての通り、フィルタと違って更新可能なパラメータが出てこないので、特に訓練により更新されるパラメータはありません。

この項のまとめ

  • プーリング層は、位置に対する感度を下げるかわりに、位置変化に対する認識能力を上げる層である
  • 畳み込み層と同じ枚数の出力が得られる
  • ストライド$s$の時、画像サイズは約$1/s$まで小さくなる
  • 更新されるパラメータは存在しない

識別部

 上記の特徴抽出部にて抽出された特徴量を使って、その画像がどのカテゴリに属するかを判断する、つまりパターン分類を行うのが後半の識別部です。
専門用語では「多層パーセプトロン」(multi-layer perceptron)と呼ばれる分類器がここに入っています。
CNNにおいては、この多層パーセプトロンにおける1つ1つの層のことを全結合層(fully connected layer)と呼んでいます。
これらを総合すると、CNNとは、入力画像に対し、「畳み込み層」「プーリング層」の繰り返しにより特徴量を抽出した後、「全結合層」の繰り返しによりパターン分類を行うシステムということができるでしょう。 

再びCNNの構造の図を出しますが、○が縦にずっと並んでいるもの1列をひとつの全結合層と数えます。そして、それぞれ一つ一つの○のことをユニット(unit)またはニューロン(neuron)と呼びます。
後者は、ニューラルネットが動物の脳の働きを真似て作られたものであることから、神経細胞を意味するニューロンになぞらえて名づけたものです。ここではユニットと呼びます。
 一番最後の全結合層は出力を直接出すので出力層(output layer)と呼ばれていて、先ほど説明したように、それぞれ対応するカテゴリである確率がそこに出てきます。
そのため、ここはカテゴリの数と一致している必要があります。例えば、犬、猫、人の判断をしたい場合は、出力層のユニットの数は3でなければなりません。
このことから、我々はCNNであらかじめ何を判別させたいのか決めておかなければなりません。
これは、言い換えると、例えば犬、猫、人を判断するように訓練されたネットワークならば、猿の画像を入れても勝手に猿を新しいカテゴリとしては認識してくれず、強制的に犬・猫・人のどれかとして出力することを意味します。
もし猿も認識させたいのならば、出力層のユニットの数は4つにしておき、かつ猿の画像を含む訓練用データセットにより訓練を行っている必要があります。

この項のまとめ

  • 識別部では、特徴抽出部により抽出された特徴量をもとに、パターン分類を行う
  • 出力層のユニットの数は、分類したいカテゴリの数と同じにしなければならない

全結合層

 全結合層の振る舞いをもう少しだけ詳しく見てみます。まず、全結合層はユニット(○)が複数個一列に並んで構成されています。
この数は、出力層の場合は分類したいカテゴリと一致する必要があるものの、それ以外の場合は特に決まりはありません。なので、500個や1024個、4096個といった数を自分で決めて用意します。
 図とその名前でなんとなく想像がつくように、1つのユニットは、前の層の全てのユニットと結合されています。
例えば1番目の全結合層のユニットの数が1024の場合、2番目の全結合層のユニットは、それぞれが1番目の全結合層の1024個とつながっています。
したがって、2番目の全結合層にたった4個しかユニットがなくても、$1024 \times 4 = 4096$ 個もの結合を有することになります。
前回紹介した、1000種類のカテゴリを分類することが目的の A.Krizhevsky らによる AlexNet は、出力層の直前で、4096個のユニットをもつ全結合層を2つも重ねているため、この部分だけで$4096^2 = 16777216$ 個という、莫大な数の結合を持っていることになります。
そして、その結合一つ一つに対して更新可能なパラメータ(重み, weight)が割り当てられています。つまり、ここだけで1600万個ものパラメータがあるというわけです。
 全結合層の計算は単純です。一般の場合でも同じなので、3つのユニットを持つ層1から3つのユニットを持つ層2へどのように情報が伝達されるかを見てみます。

模式図としてはこのように書けるでしょう。それぞれのユニットは、前の層、あるいは次の層の全てのユニットと結合をしています。そして、1つの結合につき専用の更新可能なパラメータ、つまり重みが割り当てられています。
図の場合は、$3 \times 3 = 9$ 個の重みが存在しています。

第二層の1つめのユニットを計算したい場合はこう書けます。ここで1つめ、2つめのユニットというのは、図で見た時に上から1つめ、2つめであることに対応するものとします。
それぞれに専用の更新可能なパラメータ、重みがあるわけですが、これは$w_{ij}$という風に書いてあります。
例えば $w_{21}$ というのは、第一層の2番目のユニットから第二層の1番目のユニットに対する重みという意味です。
 これではわかりにくいので、出力と重みを実際の数字に置き換えて確認します。

このとき、現在注目している第二層の1番目のユニットの入力は、前の層のそれぞれのユニットの出力と重みをかけあわせ、足し算したものになります。
畳み込み層の時にも出てきた「内積」という計算です。
例えば上の場合、第二層の1番目のユニットは

$1 \times 5 + 1 \times 1 + 4 \times 4 = 22$

という値を受け取るわけです。
 ただし、実際には、1つのユニット計算する際、パラメータはこの3つに対して1つ増えます。
それは、よりネットワークの表現力を高めるため、バイアスと呼ばれる「原点からの浮き」のようなものを表す要素が、こうして計算された値に加えられているからです。
よく直線の方程式を $y=ax+b$ とか書いたと思いますが、この時の $b$ のようなものです。
このバイアスも、1つのユニットごとに専用でパラメータとして用意されるため、結局、第二層のユニット1つ1つが、第一層からの出力に対する重み3つとバイアス1つの合計4つの更新可能なパラメータを持つことがわかります。
 こうしてユニットの入力値を「重みとの内積+バイアス」によって得た後、最後に活性化関数(activation function)と呼ばれる関数に通して出力とします。
簡単に言うと活性化関数を通すことで「出力を複雑化」しています。専門用語で言うと「非線形性」というのを高めています。
これによって何が嬉しくなるかですが、これも簡単に言えば「ネットワークの表現力が向上」するのです。
つまり、そのままユニットの入力値を出力にした場合、人と犬と猫を分けられるような計算が再現できないかもしれないところ、活性化関数を挟んで表現力を向上させることで、再現できやすくしています。

この項のまとめ

  • 全結合層におけるユニットは、前の層の全てのユニットと結合している
  • それぞれの結合に対して更新可能なパラメータを独立して持っている
  • 全結合層では、前のユニットの各出力値とそのパラメータとで計算を行った後、活性化関数というものを通すことで表現力を向上させる

活性化関数

 活性化関数をユニットの入力に対して適用することで、最終的な出力にします。これにより、ネットワークの表現力を高める働きがあります。
しかしながら、関数なら何でもよいかというとそうでもなく、うまく訓練ができるようないくつかの関数が知られており、それらが主に用いられます。
典型的な多クラス分類CNNを操作する限り、知っておかなければならない関数は2つで、それはReLUと呼ばれる関数と、ソフトマックスと呼ばれる関数です。
 ReLUとは、Rectified Linear Unit の頭文字をとったもので、名前が難しそうですが非常に簡単です。
それは単純に、入力が0より大きければそのまま出力とし、0より小さければ0にして出力するというものです。
無理やり式で書くと

$y = \max(0,x)$

です。多クラス分類の場合、従来はシグモイド関数 $f(x) = 1/(1+e^{-x})$ と呼ばれるものが使われてきましたが、ReLU関数の方がうまく学習ができることから、こちらが主流となりました。
面倒な場合は、「とりあえずReLUとかいうものを指定しておけば大丈夫」とだけ覚えておいてください。
 一方でソフトマックス関数は、出力層に対してのみ用いられる特別な活性化関数で、その層の出力値の組み合わせを確率に変換します。
今までの計算は単純な掛け算足し算で、出力が必ず 0〜1、全部足したら 1 になる確率として出てくる保証は全くありません。
なので、出力値を 0〜1、その層の出力値を全部足せば 1 (100%) になるよう変換するための関数がソフトマックス関数というわけです。
特別な活性化関数と書いたのは、通常活性化関数はユニットの入力に対して作用する1変数関数なのですが、ソフトマックスはその層の全部の値を考慮して各ユニットの値を決めているからです。
細かい話は忘れるにしても、「とりあえず出力はソフトマックスという名前の関数、あとはReLUという名前の関数にしておけば大丈夫」とだけ覚えておいてください。
 なお、畳み込み層、プーリング層についても活性化関数は同様な原理で適用可能ですが、こちらの場合、プーリング層自体が活性化関数的な役割(非線形性の付与)を持っていることから、必ずしもこれらに活性化関数を通す必要はないようです。
実際、Caffeの場合、明示的に書かない限りは畳み込み層とプーリング層、全結合層に活性化関数は適用されず、各サンプルでも、ほとんどの場合、全結合層以外の層に活性化関数は使っていないようです(詳細はこちら)。

この項のまとめ

  • 出力層においては、ソフトマックスと呼ばれる関数が用いられ、これにより出力がそれぞれのカテゴリである確率に変換される
  • それ以外の全結合層では、ReLUという関数が主に使われている

ここまでのまとめ

ここまでで、以下に示す典型的な多クラス分類CNNの構造、および各設定項目について説明しました。

また、CNNは分類を行うために必要な情報である特徴量を画像から抽出する特徴抽出部と、それらの特徴量を元にパターン分類を行う識別部に分かれることを説明し、
特徴抽出部においては、畳み込みフィルタを任意枚数備えた畳み込み層、位置に対する感度を下げるかわりに位置変化に対する認識能力を向上させるプーリング層からなることを説明しました。
そして特に畳み込み層は、主に奇数サイズで正方形のフィルタが複数枚あり、その枚数と同じだけの出力画像が得られ、また畳み込みという演算は、処理後の各点の値を決定するのに他の点の値も考慮する計算であることを説明した後、
プーリング層においては、一定のサイズの領域ごとの最大値を代表値とするMAXプーリングを用いるのが主であり、その刻み幅(ストライド)に応じて出力サイズが小さくなることを説明しました。
 一方で識別部は多層パーセプトロンと呼ばれる識別器からなっており、中身は全結合層が複数連なっているものであることを説明しました。
全結合層の各ユニットは前の層の全てのユニットと結合を持ち、それぞれにパラメータが独立に割り当てられていて、そのパラメータと前の層の出力とで計算を行った後、活性化関数と呼ばれる関数を通すことでネットワークの表現力を高めることを説明しました。
最後に、活性化関数は主にReLUという関数が用いられ、出力層は確率にするためソフトマックスと呼ばれる関数が用いられていることを説明しました。

誤差関数

 ここまでの説明は、CNNに画像を入力した時、どのような出力が得られるかという話でした。
前回も書いたように、CNNは、そのように出した出力と、理想の出力値との違いを数値化してパラメータを自動更新することで未知データに対応できるようになっていきます。
 例えば人、犬、猫を分類したい場合、出力は3つあり、それぞれ人である確率が0〜1 (100%)、犬である確率が0〜1, 猫である確率が0〜1として、3つ合わせると1.0(100%)になるような形で出力されます。
もちろん犬と猫を迷った場合は $[0, 0.4, 0.6]$ といった類の出力もあり得ます。もし答えが猫ならば、理想の答えは $[0, 0, 1]$ のはずです。
そこで、現在のパラメータにおいて得られた出力と理想の答えとの違いを誤差関数(loss function)というもので数値化させます。
 訓練用のデータセットがあるとき、全部のデータを通した時の誤差の合計が小さければ小さいほど、理想のネットワーク、つまりうまくネットワークが学習できているといえます。
なぜなら、理想のデータと同一なら、誤差は生じない、つまり0だからです。
そのため、我々は何らかの形で誤差関数を定義して誤差の大きさを算出し、それを小さくする、つまり最小化させる必要があります。
結局のところ、答えと全く一緒であれば0、違えば違うほど大きければよいので、誤差関数として様々な形態が考えられますが、画像分類においては交差エントロピー誤差関数(cross-entropy loss function)と呼ばれるものが用いられます。
詳しく書くと数式が出てきてしまうので省略しますが、交差エントロピー誤差関数を最小化することは「画像を入力した時、理想の答えが得られる見込み」を最も大きくすることに相当します。
Caffeを使う限りは、この関数について詳しく立ち入る必要はないので、ここではやはり「交差エントロピー誤差関数とかいうもので間違いを数値化している」とだけ覚えておきましょう。

この項のまとめ

  • 訓練の際、ネットワークの出力と理想データとの違いの度合いは誤差関数という形で数値化される
  • 訓練用データセットに対する誤差関数を最小化するように各内部パラメータを更新していく
  • 画像分類の場合は、交差エントロピー誤差関数というものが主に用いられている

勾配降下法(GD)

入力データと理想のデータ、つまり正解データとの誤差を数値化したものが誤差関数ですが、それともとに、その誤差関数を小さくなるようパラメータを更新しなければなりません。
つまり、適当に決まっていた重みやバイアスといったパラメータの値を、誤差が小さくなるように変化させる必要があるということです。
 実は、この誤差を小さくできる方法が存在します。それが勾配降下法 (gradient descent method)です。略して GD とも呼ばれます。
簡単に言うと、誤差関数 $E$ に対する勾配 $-\nabla E$ というものを計算をしますが、$\nabla$が顔文字の口にしか見ない人は、別にそれでも実用上はかまいません。
勾配というものは、勾配をとる点からみて、わずかに進んだとき、幾何的に関数が最も小さくなるような方向を向くベクトル(方向を持った矢印)になることが知られています。
意味がわからない場合は、放物線 $y=x^2$ を思い浮かべてみてください。この関数の勾配を取ると、関数を図形に見立てた時、現在地点に対して必ず小さくなる方向、つまり現在地点から、確実に標高の低い場所に降りられるための次の1歩の方向が分かるという意味です。
そこで、この勾配に対して適当に小さな値 $\varepsilon$ をかけ、現在のパラメータに $-\varepsilon \nabla E$ を加えることで、誤差関数は必ず小さくなるというのが勾配降下法です。
 $-\nabla E$ で小さくなるなら、そのまま足すか、むしろ小さな値でなく100や1000をかけて足せばいいと思ったのであればそれは間違いです。
なぜなら、勾配というのは微分の一種であり、現在地点から「わずかに」進んだ時、確実に標高を下げられるというものだからです。
したがって、進む量を「1歩」にするために $\varepsilon$ という小さい値をかける必要があります。この値はいくつであるべきだというものはなく、場合によって自分で適当に設定するパラメータ(ハイパーパラメータ)です。
CNNでは0.01や0.001といった値が良いスタート地点になるでしょう。ニューラルネットの世界では、このパラメータを学習率(learning rate)と呼びます。
この値が大きすぎると、必ずしも誤差関数が小さくなるとは限らないので誤差が全く減らない現象に遭遇する、つまり学習がそもそもできないおそれがあります。
逆に小さすぎる場合、学習はできるでしょうが、無駄に進みが遅く、時間を要してしまうという悪影響が考えられます。最初は適当な値を設定しておき、もし学習が遅かったりする場合は、自分で値を調節して妥協ラインを見つけましょう。
 一応、$\nabla$が顔文字ではなくてベクトルの微分記号であることを知っている人のために、これは比較的手短に説明できるため、学習率が微少でなければならない理由を書いておきます。分からない場合は読み飛ばしてもなんら問題ありません(今後二度と使わないので)。
$\nabla f$はその定義から微少移動を示すベクトルである $d\boldmath{r}$との内積を取ると、スカラー関数 $f$ の全微分になることが知られています。すなわち

$df = \nabla f \cdot d \boldmath{r}$

と書くことができます。ただし、微少移動量というのは数学で議論する際の便利な記号にすぎず、コンピュータで実現するなら具体的な値を指定する必要があります。
そこで、具体的に移動する量を $d \boldmath{r} = -\varepsilon \nabla f$ と選ぶと、誤差関数の微少変化量に相当する $df$ は

$df = – \varepsilon |\nabla f|^2$

と書けます。つまり、 $ |d \boldmath{r}|$ が微少量とみなせる限りは、全微分 $df$ は必ず負になるため、誤差関数が減少することが保証されます。
これを微少量にするために $\varepsilon$ は小さくなければならない、ということです。大きくなってしまうと、そもそも内積の結果が全微分 $df$ に一致するという前提が崩れるため、誤差関数が減ることは保証されなくなります。

この項のまとめ

  • 勾配降下法と呼ばれる方法で誤差を小さくする
  • 進む量が大きすぎてはならないため、学習率という小さい値を勾配にかけて調節する

確率的勾配降下法(SGD)

 これで自動でパラメータが更新できるようになってめでたしかというと、そうはいきません。確かにこれで訓練はできるのですが、訓練データの数を考えてみてください。
今回の場合、MNISTでも訓練データは60,000点あります。つまり、60,000個分全体の誤差を計算しないと誤差の減る方向が分からないといっているのです。
この計算量は莫大で、しかもデータ自体、メモリに収まりきらないおそれがあります。
 そこで、その問題を解決するため、ニューラルネットでは確率的勾配降下法 (stochastic gradient descent method) 略して SGD と呼ばれる方法が採用されています。
全部のデータを使えば誤差が小さく方向がわかるが、それではメモリや計算量コストの問題があります。
SGDは、全データの中から少数のサンプルだけを抜き出し、その時の誤差を全体の誤差とみなしてパラメータを更新する方法です。
パラメータ更新の計算コストが小さく、学習の際のメモリ消費量が押さえられるという利点があります。
 また、例えば60,000点ある中の64点だけ抜き出して全体の誤差としたのでは、明らかに全体を確実に代表できる保証もないので、パラメータが思いもよらぬ風に更新される可能性があります。
これは必ずしも悪いことではなく、もちろん誤差が増えてしまうことも多々ありますが、落とし穴にハマって本当の底ではないところで行き詰まってしまった場合でも、偶然に抜け出せる可能性があることを意味します。
 この時の全体を代表させる一定数、上の例で言う64という数のことをバッチ数(batch size)と呼びます。バッチ数64の場合は、64個に対して誤差を計算し、その64個毎に同時にパラメータも更新するというわけです。
この1セットのことを1バッチと呼びます。バッチ対して、データセット全体、上の例の場合でいう60,000個のことを1エポックと呼びます。
 なお、SGDによりパラメータは更新できるのですが、一般に何十万、何百万またはそれ以上のパラメータがあるニューラルネットワークに対してこの誤差関数の勾配を計算することは容易ではありません。
実際には勾配が個別に直接計算されているのではなく、誤差逆伝搬法(バックプロパゲーション)というテクニックにより、勾配を間接的に単純計算で高速に求めてパラメータを更新しています。
勾配を求めるための変数が出力層から得られ、次にその値を使うことで出力層の手前の勾配を求めるための変数が得られ・・・というように、出力から入力に向けて勾配が求まるために、このような名前が付いています。
ここでは数学やコンピュータアルゴリズムを習得することが目的ではないので、概説的な内容にとどめておきます。実用上はバッチサイズのことさえ知っておけば後は忘れても十分です。

この項のまとめ

  • 勾配降下法はコスト面で不利な点が多いので、データセットの一部だけを使ってパラメータを更新する確率的勾配降下法が用いられる
  • データセットから抜き出す数のことをバッチ数といい、そのバッチで学習を行う1単位のことを1バッチと呼ぶ

今回のまとめ

 これでCNNの構造と学習の際に必要なハイパーパラメータについてひと通り紹介しました。次は、実際にCaffeが読み込める形式(prototxt)でそれらを書いていきます。

Caffeを使ったCNNによる手書き文字分類 [1] – CNNの概要、データセットの準備

このシリーズでは、Caffeと呼ばれるニューラルネットワーク向けライブラリを使って、0から9までの手書き文字を分類できるような畳み込みニューラルネットワーク(CNN)を分類させる方法について説明します。

  • ディープラーニングに興味があり、ニューラルネットワーク(CNN)で画像分類器を作りたい
  • Caffeを導入したが、何をどうやって使ったらいいのかまったく見当がつかない
  • Windowsメインであり、Caffeのために初めてUbuntuを導入した

ここでは以下のことはすでに知っている・やってあるものとして話を進めます。

  • Ubuntu 14.04をインストール済み
  • Caffe最新版(2016/02/19時点での)を正しくインストール済み (testが通っている)

CNNとは?

 畳み込みニューラルネットワーク(CNN, Convolutional Neural Network)とは、生物の脳における視覚システムにヒントを得た情報処理の手法です。この由来から、神経を意味する neural の名前がついています。
別に、アメリカのニュースを放送しているテレビ局とは関係があるというわけではありません。
 わざわざ「畳み込み」とかいう変な名前がついていますが、それがついていない普通のニューラルネットワークもあります。
これらはいずれも「機械学習」と呼ばれる、与えた情報がどのようなパターンに属するのかを判断するパターン分類に代表される大きな分野の中の1つの小分野として位置づけられます。
機械学習という名前から想像が付くように、これらは「訓練」と呼ばれる過程をそれぞれのモデルに行うことで、実際に使える状態に自らを変化させていきます。
つまり脳のたとえで言うなら、人間と同じように、ある入力に対して正しい反応ができるよう、脳の神経細胞同士の繋がりの度合いを自ら強めたり弱めたりするようなことをして学習していくのです。
 そういう意味ではこの2つは学習のメカニズムはほぼ同じものですが、CNNの場合、普通のニューラルネットに加えて、入力情報の周囲のつながり、画像の場合は上下左右の位置関係になるわけですが、それを考慮しながらより抽象的な情報へと変換してき、それを普通のニューラルネットワークでパターン分類するという2段構成になっています。
このように、周りの情報を考慮しながら情報を抽出するという特性により、主に畳み込みニューラルネットワークは画像認識の分野で非常に広く用いられていて、実際非常に高い性能を発揮しています。
 もしここまでで書いたことがよく分からない場合は、それでも問題ありません。
とにかくこのシリーズでは、コンピュータビジョンや機械学習、アルゴリズムなどの専門的な知識は仮定せず、CNNの構造をできるだけ誰でも分かるように、かつできるだけ詳しく書くことを目標にしているので、最後まで読み進めるようにしてください。

データセットの重要性

まず初めての人が勘違いしてはならないのは、ニューラルネットワークには訓練データが必要だということです。
私達が自分で入力データおよびそれに対して出てきて欲しいデータを用意し、ニューラルネットが出した答えに対して具体的な間違いを指摘してやる必要があります。
その結果、ニューラルネットはその間違いに気付き、正解に近づけるよう学習をしていきます。
これを繰り返すことで、未知のデータがやってきても、問題なく判断できるモデルが出来上がるのです。
 こう書くとまるでニューラルネットワークが魔法で動いているように思ってしまうかもしれないので、一応コンピュータがちゃんと計算できる仕組みになっているということを補足するため、
より詳しくこの流れを書き直します。CNNを含むニューラルネットには、更新可能な内部パラメータが大量にあります。
これらのパラメータは、最初はランダムで決めてあり、与えられた入力データを元に、それらのパラメータに基づいて計算がなされ、結果が得られます。
例えば、CNNで「人」「犬」「猫」を分類したいとしましょう。ここで、パラメータはランダムに決まっているので、人の画像をCNNに入力したとしても、CNNはほぼランダムにどれかを出力します。
つまり、最初は人を入力しても人、犬、猫のどれかわからないで適当に答えます。定期テストで、問題の意味が全くわからない3択問題が出た時、鉛筆転がしで答えを決めたようなものです。
 そうして出てきた結果に対して、「違う」ということをCNNに伝えます。具体的には、CNNの場合は各カテゴリの「確率」を出力します。
例えば上の例の場合、全くどれがどれか分かっていないため、人、犬、猫どれも33%という結果が出てくるというわけです。
そこで誤差関数というものを定義して、その違いの度合いを数値化して伝えます。「それは人の画像であり、人は100%、残りは0%になるべきだったんだ」ということを、今出てきた全部33%の数値との誤差という形でニューラルネットに伝えるのです。
 これを受けたCNNは、誤差逆伝搬法という、高速にパラメータを更新するための方法を使い、確率的勾配降下法という、確実に正解に近づける、言い換えると確実に誤差を減らすような方法でパラメータを自動で更新します。
これを何百回、何千回、何万回と実施することで、はじめて分類器として機能できるようになります。
 ここで強調したいのは、○○法とかいう変な名前の難しそうなコンピュータアルゴリズムについてではなく、ニューラルネットは訓練データとそれに対する正解データを与えて訓練しなければ使えるようにならないということです。
分類器に限らず、何をさせるにあたっても、ニューラルネットは訓練のための入力データ、およびそれに対する正解データの組を必ず、それも多くの場合大量に(少なくとも数千〜場合によっては数百万)必要とします。
 CNNを使って分類器を作りたいと思ったら、どういう構造にするかとか、細かいところはどうでもいいので、「どうやって訓練データを用意するのか」、つまり、
入力画像とそれに対する正解の分類結果をどうやって大量に用意するのか」ということをまず考えてください。
そもそも訓練のためのデータが用意できなければ、どんなに素晴らしい構造やタスクを思いついても、最終的にモデルが構築できないので意味がありません。
例えばあなたがアニメが大好きで、アニメキャラクターAとアニメキャラクターBの区別がCNNで付くのか試したければ、それぞれのキャラクターの画像を少なくとも何百枚かは用意しなければなりません。
しかも、どちらがどちらなのか、ファイル名、フォルダ分けなり、テキストファイルなりで区別できるような状態になっている必要があります。
 さらに、必要なのはこれだけではありません。訓練したら、結果としてどれくらい正解できるようになっているのかを測定できなければならないからです。
訓練をしたらそれでおしまいでは、万が一正しく訓練できていない場合、いざ実用の段階に入った時に全く分類ができていないという悲惨な結果が待っているかもしれません。
したがって、訓練データ同様に、CNNの性能を見るため、入力データと正解データの組をある程度の数は別途保持しておく必要があります。この正答率を見るためのデータをテストデータといいます。
テストデータの用意の仕方は2種類考えられます。まずは単純に大量にある訓練データの中から一部(10%〜20%くらい)を拝借してそれらをテストデータに作り変えることです。これは簡単です。
もうひとつは、全く別のデータセットを用意することです。基本的には前者のアプローチでいいのですが、場合によっては後者の方法が適切な場合もあります。
 例えば、あるアニメシリーズにおけるキャラクターAとキャラクターBの分類をしたいとき、第1期と第2期で製作会社が変わって全く絵柄も変わったとします。このような場合、第1期の映像からだけ
2キャラクターを持ってきて訓練データとテストデータを作ると、確かにテストデータに対する正答率は訓練により高まっていくのですが、
それで分類器ができたと思い込み、いざ第2期の同じキャラクターの画像を入れると、絵柄が大きく違うためにほとんど正しく分類できないという現象が生じる場合があります。
このように、訓練データ(またはそれを流用したテストデータなど)にしか成果を発揮できず、それ以外のデータに対しては成果が低くなってしまう現象を過学習(オーバーフィッティング)といい、
このようなおそれがある場合は全く別のデータセットをテスト用として用意するのが無難です。

この項のまとめ

  • ニューラルネットは訓練をしなければ使えるようにならない
  • CNNの訓練には大量の入力画像と正解データの組を必要とする
  • 訓練してモデルを構築する際には、テストデータで正答率を見ることにより性能を確認する
  • モデルが訓練データのみに対して適用している状態(過学習)は避けなければならない

手書き文字分類とは

ここでは、1989年にY.LeCunらによって提案されたLeNetという、元祖CNNとでもいうべきニューラルネットを提案した論文における目標であった「手書き文字分類」を、厳密な仕組みは若干違いますがLeNet同様のCNNを構築することで行います。
ちなみに、CNNが本格的に注目されるようになったのは2012年ごろになってからです。なぜそんなにすごいものが放置されてきたのかと思うかもしれませんが、もともとCNNの構造そのものはこの時代から何ら変わっておらず、
変わったのは大量のデータが手に入るようになったこととコンピュータの処理能力が大幅に上がったことです。「時代が追い付いてきた」というのが現状を説明するのに適切な表現かもしれません。
 この手書き文字分類では、以下のような28×28ピクセルの正方形でできている手書き文字画像が0から9のどれなのか分類することを目標とします。

mnist.png

これは8枚の画像を横に連結しているのですがそれぞれ5, 0, 4, 1, 9, 2, 1, 3であることが理解できます。このような手書き文字分類を行う際には、
先ほど説明したように、0〜9の10種類の手書き文字画像、およびそれに対応する正解データを大量に用意する必要があります。
そこで、その手間を省くため、世の中には様々なタスクに応じた「データセット」と呼ばれるものが転がっています。
何かやりたいと思いついた時は、まずそのやりたいことにふさわしいデータセットが無料で手に入らないか探しましょう。なければ残念ですが自分で作らなければなりません。
先程も書きましたが、どういう構造にしたらいいかなどは後回しであり、データセットの目処がたった後です。
実は、ニューラルネットワークの世界では、コンピュータで計算するくらいなので、訓練するためのアルゴリズムはきっちりと定まっているのですが、なぜそれが良いのか、どのような構造なら最も良いのかという本質的な部分については理論的に解明されているわけではありません。
何が言いたいかというと、新しい方法やタスクを思いついたとしても「やってみなければ分からない」、言い換えれば試行錯誤的な側面が非常に強く、それを検証するには訓練するためのデータがなければならないということであり、データセットのことは常に念頭に置いて欲しいということです。

 手書き文字分類においては、幸い上の画像のような28×28の手書き文字画像が訓練データ60,000枚、テストデータ10,000枚収録されたMNISTと呼ばれるデータセットが存在するのでこれを使います。
LeCunらがもともとLeNetを学習するために使ったのもこのMNISTです。
その他、ディープラーニングの分野で非常に有名なデータセットとしては、A.Krizehvskyらが2012年に発表し、1000カテゴリの一般物体認識タスク(犬、猫、鳥・・・)の正答率を競うコンテストにおいて
非常に高い性能を示したことで注目されたAlexNetの訓練に使われた、Imagenetというデータセットがあります。これも一般の人が無料でダウンロードできますが、こちらは小規模バージョン(コンテスト用)ですら100GB近くあるので上級者向けです。

Caffeにおけるデータセットの扱い

ここからは、どのライブラリを使っていたとしてもCNNを手段にする限りは共通する一般論ではなく、Caffe特有の取り扱い方に入っていきます。
Caffeでは、より高速に、効率的に訓練を行うため、jpgやpngといった画像をそのままCNNに食わせるのではなく、LMDBという形式にデータセットを変換したものを与えます。
普通の画像を1枚ずつ食わせることもできますが、面倒だと思ったとしても、基本的には訓練・テスト用データセットにはLMDBを作るべきです。
正解データをテキストファイルとして記述したものと画像ファイルを用意しておくことで、LMDBは一行で作ることができますが、この作り方は別のチュートリアルで紹介します。
今回のMNISTデータセットに関しては、嬉しいことに、最初からLMBDが完成した状態でダウンロード可能なので、それを使うことにします。

MNISTデータセットの準備

 ここでは、 “/home/ユーザー名/caffe/” が Caffee のルートディレクトリ(一番上のフォルダ)だとしましょう。
各ユーザーの一番上の階層のフォルダが “/home/ユーザー名/” であり、普通はここに caffe がインストールされているはずです。
次の手順で、MNISTの生データ(LMDBになる前のデータ)を取り寄せます。

  • 左側のメニューの一番上にある、Windowsボタンに相当するUbuntuボタン(?)を押す
  • “term” と入力して端末(Windowsでいうコマンドプロンプトのようなもの)を開く
  • “sh /home/ユーザー名/caffe/data/mnist/get_mnist.sh” と入力するとスクリプトが実行される

Ubuntuでは、ファイルの移動や削除といったレベルのことはWindowsと同じ感覚でできるものの、多くの作業は黒い画面(端末)で行うことになります。
sh とは、その黒い画面上で打ち込むコマンドを一連の処理として1つのテキストファイルにまとめたもので、シェルスクリプトと呼ばれています。
これを実行するための命令が sh というわけです。 “sh ファイルパス” でこのようなシェルスクリプトを実行できます。
 get_mnist.sh を実行してしばらく待つと、ダウンロードが完了して、get_mnist.sh と同じフォルダ(/home/ユーザー名/caffe/data/mnist/)に変なファイルが4つ登場します。

1.png

生データを取り寄せたら、次の手順を実行することにより、LMDBに変換します。これもスクリプトが適切な変換手順を実行してくれているのですが、
ここではどれも1回しか使わないので中身については気にせず、自動でなにかやってくれてるくらいの認識でも問題ありません。

  • cd /home/ユーザー名/caffe/
  • sh examples/mnist/create_mnist.sh

手順としては、 cd というコマンドで caffe のルートディレクトリに移動します。その後、その中にある create_mnist.sh を実行しています。
なぜさっきは移動しなかったのに今回は cd で移動したかですが、単純に create_mnist.sh が、現在参照しているディレクトリが Caffe のルートであることを前提に組まれているというだけのことです。
以下のような画面が出たら正常にLMDBが生成されました。

myusername@myusername:~$ cd caffe
myusername@myusername:~/caffe$ cd /home/ユーザー名/caffe/
myusername@myusername::~/caffe$ examples/mnist/create_mnist.sh
Creating lmdb...
Done.

このように、 create_mnist.sh があるフォルダである /home/ユーザー名/caffe/examples/mnist/ に行ってみると、 mnist_test_lmdb というフォルダと mnist_train_lmdb というフォルダができていることがわかります。
それぞれが名前の通りテスト用のLMDB、訓練用のLMDBです。このLMDBを、わかりやすい場所にコピペします。今回は、 /home/ユーザー名/mycnn/ というフォルダで一連の作業をしたいことにしましょう。
この2つのフォルダを選択して右クリックし、「コピー」を選びます。

2.png

今回、CNNを構築するために作った作業フォルダ /home/ユーザー名/mycnn/ に移動し、右クリックして「貼り付け」を選ぶことで、貼り付けます。

3.png

これでMNISTのデータをLMDB化したものを調達することに成功しました。自分でデータセットを作る際でも、訓練用、テスト用と2つのLMDBを作る必要があります。
同じLMDBを指定してしまうと、Caffeでいざ訓練をするという時、テスト、訓練で同じファイルを2度同時に開こうとしてしまうためにフリーズします。

今回のまとめ

今回は、ディープラーニングにおけるデータセットの重要性を説明しました。また、LeNetというCNNの祖先が目標とした手書き文字認識のタスクを追体験するため、LeNetが使ったのと同じデータセットであるMNISTをダウンロードしました。
次に、Caffeを訓練する際には画像ではなくLMDBというファイルに変換して行うことが好ましいことを紹介し、MNISTのデータをLMDB形式で訓練用、テスト用と作成しました。

微分の概念と諸法則

ここでは、通常高校の数学IIと呼ばれる科目でやることになっている、微分の概念と各種法則について紹介します。しかし、多くの教科書のように、いきなり「微分」とかいう計算から始めるとやる気がなくなる恐れがあるだけでなく、今更記事を作る意味も存在しないため、ここではその典型的な応用例を通して理解を試みます。
 まずは、微分をいきなり導入するのではなく、「ルートの求め方」について考えます。その上で、ルートを求めるためには方程式の解を求める手法である「ニュートン法」という方法があることを紹介し、またその原理を図的に理解した上で、その手法において「接線」というものが必要であることを紹介します。さらに、接線を求める方法について検討する過程で、自然に微分を導入していき、最終的には微分の各種法則についても検討します。

ルートを求めるには

 電卓をたたけば当たり前のようにルート2は1.41421・・・と表示されますが、そもそもどういう原理でそれが可能でしょうか。ルート2を表示しているのは、別にコンピュータがルート2のことを1.41421・・・という値として記憶しているわけではなく、何らかの命令を通してこの値を算出しているのです。そうでなければ、素数などいくらでもあるのでルートもいくらでも存在し、コンピュータがとうてい覚えられるものでもありません。それに、そうなるとそれを打ち込むのは人間なのでとてもやっていられません。もちろん、いろいろなプログラミング言語を使えばルートを一発で作る命令は用意されていますが、それも何らかの計算を組み合わせたものが提供されているにすぎず、結局、電卓を含む、コンピュータにできることは本来四則演算を組み合わせることだけのはずなので、四則演算だけを使うことでどうにかそういう値を求められる方法を検討する必要があります。
 ここで一つ注目しておきたいのが、コンピュータに「方程式を解かせる」ことは可能だということです。なぜなら、 (左辺) = (右辺) という形になっているので、左辺と右辺に適当な値を代入して、それが一致していれば答えになることが判断できるからです。考えるのは少しばからしい例ですが、「x=2」があったとするなら、右は2です。左のxに1やら0を代入しても、2にはなりませんが、2を入れれば当然右の2と一致します。ということは、それが答えというわけです。以上から、なんとか四則演算に値の比較を交えればコンピュータでも方程式を解くことは可能ということが理解できます。
 ということは、ルート2を求めたければ、ルート2が答えになるような方程式を作り、四則演算の繰り返しで確実に答えになるような方法を編み出せばいいということになります。
 ルート2というのは、「2回かけたら2になる数」という意味です。ルート3なら「2回かけたら3になる数」です。結局ルート○○というのは、「2回かけて○○になる数」という意味なのです。そうであれば、「ルート○○×ルート○○ = ○○」が成り立たなければなりません。というより、これこそがルートの定義です。これを方程式にするのであれば、ルート○○がいくつかを知りたいので、これを x とでもして「xの2乗 = ○○」とすればよいですね。この答えを我々は綺麗に表示する方法がないために、わざわざルートという記号を導入して、ルート2と書いているわけです。
 以上より、ルート2を求めるには

$$x^2=2$$

という方程式を解けばよいということになります。あとは、x に1だとか1.1など色々入れて、右辺と左辺が一致すればそれが答えです。
 その前に、少しだけ形を変えましょう。具体的には、「(式) = 0」となるように変形します。この場合は、右の2を左に持ってくればよいだけで

$$x^2-2=0$$

です。このようにすれば、左辺の x に適当な値を入れて、それが0になるかで答えが判断できます。より簡単です。

ニュートン法の原理

 あとは、どのようにして左辺が確実に0に近づくように x を定めていくかについてです。この近づけ方はいくつか方法が考えられていますが、中でも特に効率がよいのがニュートン法と呼ばれている手法です。名前だけで難しそうだと思う人がいるかもしれませんが、ニュートン法というのは至って簡単な手法です。確かに式に起こして議論するなら一般的な小学生では理解できないでしょうが、図形的にそれを実践するのは小学生でも余裕でできます。
 まずは、状況を図で整理しましょう。ある方程式があって、その答えを知りたいとします。ちょうど上のように、項を移動した結果 f(x) = 0 という形式になっています。今回はxの2乗-2ですが、別にここは何であっても同様です。

1.jpg

そもそもなぜこの図が描けるかというのを思い出しましょう。いま、適当な方程式

4.jpg

があるとします。これは、見方を変えると

5.jpg

という2つのグラフがあって、その y をつないだものと見ることもできます。 y をつないだということは、2つのグラフの y 座標は同じであることを意味します。その上で、この方程式の答えがその点の x 座標ということになるのです。ここで y = 0 とはもちろん x 軸のことを意味します。したがって、上の方程式

4.jpg

は、「任意の関数が x 軸を横切る点の x 座標を求める」ということを意味します。そのため、このような図が描けるのです。
 実際の近づけ方です。まず、x 軸上に適当な点を取ります。言い換えれば、「だいたい正解はこのくらいだろう」と思われる適当な場所を指定します。

6.jpg

この場所は x0 としましょう。そうすると、関数上の点も同時に y0 = f(x0) であることがわかります。f(x0) というのは、「関数の x となっている部分に x0 を代入したもの」という意味です。たとえば、 f(x) = x で、x0 = 2 だとしたら、f(x0) というのは、f(x) の x になっている場所が x0 の値、つまり2になるので f(x0) = f(2) = 2 ということになります。
 次に、この関数上の点 (x0, y0) を通る接線を引きます。そして、接線が x 軸と交わる点を次の推定値 x1 とおきます。

7.jpg

これが現在の図です。まだ答えではありませんが、確実に答えに近づいたことがわかります。
 次にやることも全く同じです。今度は (x1, f(x1))を通る接線を作り、またその接線と x 軸との交点を次の x2 とするのです。

8.jpg

見てください。さっきよりもさらに答えに近づきました。もう一回だけ同じことをしてみましょう。

9.jpg

答えに近づきすぎたため、もはや拡大しなければこれ以上続行することはできなくなってしまいました。この手順を繰り返し行うと、いくらでも答えに近い値にすることができます。もちろん正解が正確にいくつか知ることも重要でしょうが、実用上は小数点の下は数桁程度で十分な場合がほとんどです。そう考えれば、この正解への近づき具合の早さから見るに、この手順を無限、それどころか何十、何百回もやらなくても、実用レベルのルートの値は何回か実行するだけで得られるのではないかという予測も立つでしょう。これがニュートン法の原理です。
 
 さて、図で語るのは簡単でしたし、確かに答えに近づけるのは直感で理解できましたが、図で書いただけでは数字が分かりません。結局はこれを数式として書き起こす必要があります。そして、一つだけ、義務教育では習っていない用語が登場したことに気づいたことでしょう。それが接線です。確かに、2次関数の接線など、求め方を習った覚えはありません。
 接線といえども、結局は直線です。直線は切片と傾きさえあれば引くことができます。前置きが長かったですが、今回紹介する微分は何をするための道具かというと、この接線の傾きを求めるための計算をする道具なのです。このように紹介すると、何か全く新しいものが登場して怖いと思うかもしれません、しかし、これもおそらく気づく人は少なかったでしょうが、この微分というのは全く新しいものではなく、中学の時に2次関数のところで習ったある概念の延長線上にすぎないものです。

直線の切片と傾き

 ここからは理論編といったところですが、手始めに直線について簡単に思い出します。
 学年が上がると色々なグラフを習っていきますが、一番基本的なものは直線です。教科書は難しい名前を付けるのが好きらしく、これには一次関数という名前がついています。

20.jpg

直線は y=ax+b と書けば表現できます。このときの b は切片と呼ばれていて、原点からどれだけ直線が浮いているかの意味です。 a は傾きと呼ばれていて、その直線が右方向に1進むと上方向にどれだけ大きくなるかを意味している値です。
 例えば、y=2x-6 なら、切片はマイナス6になります。ということは原点では下に6つ沈んだ場所にあります。そして、右方向に1進むと上方向に2ずつ上昇していきます。ということは、右方向に3つ進めば上方向に合計6つ上昇するので、ここで初めてx軸と横切ることがわかります。

21.jpg

こういう感じです。傾きがマイナスなら逆に下に下がりますし、切片がプラスならこれもまた図と逆で原点で直線が下方向に沈んでいるのではなく上方向に浮いています。まずは「一次関数」とかいうものをこのように見ることが重要です。y=2x-6 の例で続けますが、当然傾きが2というのは右に1つ進むと上に2つ上がるということを意味するので、逆に左に1進めば下に2下がることになります。だから、xが0の時、y座標は切片と同じ-6ですが、反対方向に1進むとx座標マイナス1でy座標はさらに2下がり、マイナス8になります。そして、右方向に2つ進めば傾きはその2倍の4つ上に上がることになりますし、半分の1/2しか進まなければ傾きの半分である1しか上に上がらないことになります。
 ここで注目すべきなのが、傾きがこの場合 “2” という一定値だということです。つまり、どこの点からスタートしても、右へ1進めば上に2上昇します。傾きというものをリアルな坂道だと捉えて下さい。直線というのは勾配が一定の斜面のことです。

平均変化率の概念

 しかし、これが2次関数だとどうでしょう。例えば x2-2 という関数だったとします。単に y=x2 のあのおなじみの曲線が、全体的に下方向に2沈んだだけのことなので書くのは簡単です。

23.jpg

 これがリアルな坂道だと思って下さい。x=0の場所がスタート地点とします。ここは緩やかでむしろさっきの直線より昇りやすそうですが、段々右へ進んで行くにつれて明らかに傾きが厳しくなっていることが分かります。右端当たりまでいくとSASUKEの仕掛けにしか見えないほど急です。ここで学べる教訓は1つです。直線と違い、曲線は傾きが場所によって違っています
 と、いうことは、「曲線の傾き」というものは直線のように「a=2だから2」などと一言で表現できるようなものではありません。逆に直線が非常に特殊で簡単な例だったのです。曲線の場合、場所によって傾きは1かもしれないし、100や-99999かもしれません。
 そこで重要になってくるのが、「その場所ではどれくらい坂道がきついのか」という目安です。要するに、無理矢理曲線を直線に見立てて、傾きを計算させるのです。例えば

24.jpg

どこでもいいですがこのように x=2 の時を見ます。このときのy座標は、関数 x2-2 の x に 2 を入れればいいので f(2)=2 です。やはり前回に引き続き縦横の尺がおかしく見えますがこれは気にしないでください。このときどれくらい傾いているかを知りたければ、この点とちょっとだけ進んだところにある点を結んだ直線の傾きを求めればよいのです。1点で直線は引けませんが(厳密に言えば無数にあるので1つに定まらないのですが)2点あれば直線が引けます。ということは傾きが分かります。

図的には完全にバランス崩壊していますがイメージの話で数字はどうでもいいので引き続き気にしないことにします。ここでは、1つ右にいったx=3をとってみました。このとき f(3)=32-2 = 7 が分かります。つまり、今居る点P(2,2)の傾き具合を知るために適当に横に取ったQ(3,7)の2点を結ぶ直線を引けば、完全に同じではないがその点の傾きが分かるのではないかということです。
 2点が定まっている時の傾きの決定方法は学校で習うかもしれませんが次の通りです。まず左の点から見て右にxがいくつ移動しているかを見ます。この場合1移動しています。そしてyは、5つ上に上昇しました。ということは、xが1増えた時にはyは5上昇するような直線ということになります。それはこの直線の傾きが5であることに他なりません。したがって、今いる点Pの傾きは「だいたい5くらいかもしれない」というのが分かりました。このときの5という傾きが、平均変化率または変化の割合とかいう難しい名前で呼ばれている数値のことです。要するに平均変化率とは「曲線は場所によって傾きが違うから、知りたい点とは違うもう1つの点を取ってその直線の傾きをその場所の大体の傾きということにしました」という意味です。

微分の概念

 平均変化率にも問題があります。それはもちろん、正確にはその点の傾きではないということです。では一体どうすればその点の傾きになるでしょうか。答えは簡単です。幅を0に近づければいいのです。上のように誤差ができているのは、幅を広く取り過ぎてしまったからです。見ての通り、右に行けば行くほど傾きの具合は大きくなっていくのですから、x=2の求めたい場所より右方向に幅を取り過ぎれば、本当の傾きよりも傾いた値が出てしまうのは当然です。
 これも実際に書いてみると分かりますが、最初はx=2ともうひとつの点としてx=3を取りました。この幅をどんどん狭めていくと、どうもx=2における接線になるらしいことが分かってきます。

26.jpg

これは幅をある程度縮めたときの直線です。この調子で狭めていけば最終的に接線になることが分かると思います。つまりその点の傾き具合というのはその点の接線の傾きです。
 ここで問題なのが、どうやって幅を0にするかということです。直線は点が2つなければ1つに定まりません。いくら幅を0に縮めようとしても、必ず目的の点からわずかに離れた場所にもう1点打たなければならないのです。
 この議論をもう少し具体的にできるよう、式にしてみましょう。上の図だとx=2の隣に打った点のことを「2からわずかに進んだ点」と表現しましたが、これでは式に書けません。この「わずかに進んだ幅」のことを適当な文字で「h」と表現します(Δx などという書き方もあり、通常は私もそう書きますが、ここでは紛らわしいのでやめておきます)。すると、その「2からわずかに進んだ点」のx座標は、

27.jpg

と表現できます。さっきは、幅が1だったのでhは1だったわけです。もちろんいきなり0にしてしまえば、始点と終点が同化し、2つの点ではなくなってしまうため直線が引けず、困ってしまいます。したがって、このhは0.1、0.001、0.00001・・・のように「0に近づけていく」(縮めていく)ことが理想です。幅をとりあえずhとしておいて、その時の傾きを求めたのち、hをどんどん小さくしたらどうなるか見てみます。このわずかに進んだ点のy座標は、関数 x2-2 の x に、2+h を入れればいいだけなので

28.jpg

ということがわかります。

29.jpg

今はこういう状況です。3が2+hという表現に変わっただけでまだ何も起こっていません。この直線PQの傾きですが、もちろんx座標は幅hだけ増えたので右にh進んでいます。一方上方向にはどれだけ進んだかですが、そのまま「高い方から低い方」の座標を引けば長さが分かるので

30.jpg

です。これで、xがhだけ増える間にyは上の分量だけ増えていることが分かりました。「xが1増えるとyがいくつ増えるか」という傾きに直すには、yの増えた量をxの増えた量で割ればよいので、

31.jpg

ということがわかりました。あとは、hを0にしたいのですが、そのままでは0に持って行けません。なぜならここでhを0にしてしまうと、分母が0になるからです。
 0で割ることは算数では禁止されていて、その値がどうなるかは考えてはいけないことになっています。確かに、1/3、1/2、1/1、1/0.5、1/0.1・・・などと分母を小さくしていく状況を考えてみれば、段々値が大きくなっていくのは理解できますが、際限なく大きくなりそうな気がします。そして結局いくつになるかは分からず不気味です。要するに算数をやる上で0で割ってもその値は何の意味もなしません。このように、hを0にしたらどうなるかという議論をしたとき、分母が0になってしまうような形の分数を不定形と呼びます。
 そこで細工をします。上の式には2乗のかっこがありますが、これを変形などすることで、もしかしたらhを0にしても値がいくつになるか計算できる形にすることができるのではないかということです。このような形に持っていく行為は不定形の解消と呼ばれています。実際に展開して計算しましょう。

32.jpg

なんとも不思議なことに、分子の計算をしたら分母は約分でき、これでhを0にしても問題ない状態に持っていくことができました。この状態でhを0にすれば、もちろん値は4です。つまり、本当のx=2の時の傾き(接線の傾き)は4だったのです。幅h=1とすると、当然ながらさっき求めた傾き5が得られます。
 
 簡単に言えば、上記の手順を一般的にしたものが微分です。上で見てきたように、微分するとはどういうことかというと
 

  • ある点Pをとる
  • そこからわずかに幅hだけ離れたQを取る
  • PとQの間の平均変化率(xが1増えるとyがいくつ増えるのか)を求める
  • 出てきた分数を計算し、不定形を解消してからその幅hを0にするといくつになるのか求める

ということをやっているだけなのです。
 確かにこれを実践すればその点の傾き具合、言い換えるとその点における接線の傾きは分かるようになりましたが、手順が面倒すぎます。そこでこの計算を全部やらなくてもいいような法則性はないのか、という話になってきますが、そこで初めて出てくるのが、おそらく微分について断片的に覚えているとしたら、真っ先に出てくるであろう「肩の数字を下ろす」という法則なのです。

定数の微分

 ここまでくると、いろんな関数に適用して法則を見つけ、楽したいというのが人情です。そして定数、一次関数、二次関数、三次関数・・・といった簡単な関数でその法則性を探っているのが数学IIの内容です。数学IIIの微分は指数関数や対数関数、三角関数で上の手順をやるとどうなるか試しているだけで別に何も専門的な内容ではありません。
 最初に定数を微分するとどうなるか試します。一般の場合で f(x)=a です。これはもはや定数なので x 座標ががどうであろうが関数は a という一定の値を取るということを意味します。要するに x 軸に平行な直線です。これは考えるまでもなく 0 になります。というのは、水平なので全く縦方向に増えていないからです。全く増えない平らだということは、全く傾いていないことを意味し、傾きはどこでも 0 です。したがって定数を微分すると 0 になります。

一次関数の微分

 x=2の場合の接線の傾きに限らず、一般の場合で見ていきましょう。例えば直線 f(x) = ax+b の場合です。任意の点P(x座標は任意のx)を取ったとしますが、そこからわずかにhだけ離れた点Qも取ります。点Pのx座標はx(任意の値という意味)、点Qのx座標はx+hです。

33.jpg

このようになっています。xの増加量は増えた分をhと言ったのでh、yの増加量は上の値から下の値を引けば良いのでf(x+h)-f(x)です。

34.jpg

これで準備が出来ました。上の式をxの増加量hで割ればよいので

35.jpg

ということになります。hを0にするまでもなく、hが自ら消えました。つまりこれは、xの値が具体的にいくつだとも決めなかったので、直線はどこにいても傾きがaで一定だということを示しています。
 したがって、「f(x)=ax+b を微分すると a」ということがわかりました。この微分したときの関数のことは導関数と呼ばれていて、この場合、ax+b の導関数は a です。導関数を意味する書き方は複数あり、

36.jpg

どちらもよくある書き方です。左はf(x)の間に 「’ (プライムと読む)」が入っています。これが導関数の印です。右は「xをわずかに進めたときの値でyの増加量を割り算して、わずかにすすめた量を0に持っていくといくつになるか」という意味合いから来ている記号で、分数のように見えます。実際微分は「yの増加量/xの増加量」という分数になっていることからもわかるように分数のような公式がいくつか存在します。右側はあるいは df(x)/dx とか書くこともあります。変数が x で明らかであれば (x) を省略して f’, df’/dx などもよく使われますが、結局全部同じ意味です。ここでは f'(x) と書くことにします。
 ここでもう一つ注目する点が、切片 b も消えているという点です。よく考えると、微分というのはその点でどれくらい傾いているかを表すものでした。別にその坂道が高度0mに存在しても、高度100mに存在しても、傾き具合は変わるはずがないし、恐怖感はさておきその坂を上るのに必要な苦労の度合いは同じはずです。要するに、微分したい関数に定数がくっついているとそれは消えます
 ここまでで微分について分かったことは2つあります。
 

  • 定数は消える
  • x (の1乗) を微分すると係数だけが残る

ということです。実際にニュートン法を使ってルート2を求めるには、もちろん f(x) = x2 -2 なので、二次関数の接線が必要です。

極限記号の導入

 あとはxの肩の数字を2,3,4・・・・と上げて検証すればいいだけですが、その前に記号で議論ができるよう、1つの新しい記号を導入します。
 微分の手順について改めて箇条書きにすると
 

  • 任意の点P (x 座標は任意の点) から h だけ離れた点Q (x 座標は x+h) をとる
  • 点Pの座標は (x, f(x))、点Q の座標は (x+h, f(x+h))
  • 点PQを比較して、xの増加量 h で y の増加量 f(x+h)-f(x) を割り、平均変化率を出す
  • 分母が0にならないよう平均変化率を計算してから h を 0 にする

ということでした。既に数度実践したように、hはいきなり0にすることはできません。もちろん、いきなりhを0にすると、ゼロ除算になってしまうからです。しかしながら、この前置きをいちいち文字で書いていては長くて面倒です。そこで、上の4手順をすべてひとまとめに式にしたものを

37.jpg

と書くことにします。「このままではhを0にできないので、式を整理してゼロ除算を回避してからhを0にする」というのを、左にくっつけた lim (右にくっついている式の極限を取るの意味) 「h→0」(その式のうち、どの文字を何の値に最終的には持って行きたいのかの意味) というもので代表しているのです。これは極限記号と呼ばれています。
 そもそも、「0に近づける」とか、ある値を直接代入しないで、「限りなく近づけていく」ことを極限をとると呼んでいます。見ての通り、直接代入できるのであれば直接代入すればいいだけのことであって、主に直接するとゼロ除算など無効な計算になってしまうが、どうしてもその値にしたい(したらどうなるのか)という時に使われる記号です。もっと簡単に言うと、「今すぐlimの下に書いてある値を代入したいが、今すぐやるとまずいので後からお願い」ということを言っているだけの記号で、こういう言い方をすると分かると思いますがただの時間稼ぎです。こういう変な記号を見た瞬間拒否反応を示す人もいるのではないかと思いますが、こういう記号は話を難しくしたいから導入されたわけではなく、その正反対でスペース節約して楽したいから導入された記号なのです。したがってまったく高級な記号ではありません。極限という言い方が難しくて嫌いな場合は時間稼ぎ記号とでも呼んでください。
 結局、上の式で言っていることは、「本当は h → 0 (hを0に近づける) というのをやりたいが、今すぐ 0 にすることは分母の 0 のせいでできないので、右の分数をうまく分母 0 でない形に整理してから 0 にしてくださいね」というのをいちいち言葉で説明するとスペースがないので記号にしただけのことです。
 以上から、任意の関数 f(x) の導関数は、数式で

38.jpg

と書けることが理解できます。右の分数はもちろん平均変化率で、平均変化率の幅hを縮めたいがすぐにはh=0にできないため、時間稼ぎした後でhを0にしてみる、ということを一行で綺麗に説明できています。

微分の諸法則 [1] – 和の微分法則

 記号はこれくらいで十分そろいました。寄り道っぽくなってしまいますが、あとは、xの2乗、3乗・・・を見る前に、今後の大きな武器となるであろういくつかの法則を検討します。1つめが和の法則です。具体的には、ある関数 f(x) と別の関数 g(x) があるとき、その足し算

39.jpg

を微分すると、

40.jpg

になるという法則です。まとめると、

41.jpg

ということですね。そもそも関数と関数の足し算など使い道があるのかと思うかもしれませんが、先ほどまさにこれをやりました。というのは、一次関数 ax+b ですが、左の項 ax を f(x)、右の項 b を g(x) とすれば、まさに f(x)+g(x) = ax+b になっています。このように、関数を複数の項に分割できる時は、わざわざ全体として難しく考えなくてもそれぞれの項だけ微分すればいいということを言っているのがこの法則のポイントです。
 
 では、実際に成立していることを確かめます。f(x)とg(x)をまとめて新しい関数 S(x) とでもおきましょう。

42.jpg

です。S'(x)が知りたいわけですが、結局このように f(x) と g(x) の和そのものを一つの関数として見てしまえば、微分の定義にあてはめるだけですぐ確認できることです。

43.jpg

結局、こう見えるだけなので、前の項で書いた f'(x) の定義式の中の f(x) が S(x) に置き換わるだけですね(上の図の関数の形は適当です)。
 もう一度、同じことを念のためやりますが、図を見て分かるとおり、増加量というのは「進んでいる方」から「進んでいない方」を引けばよいので、x(右方向)の増加量は
 
 (進んでいる方右側) – (進んでいないほう左側) = (x+h) – h = h
 
y(上方向)の増加量は
 
 (進んでいる方向上側) – (進んでいないほう下側) = S(x+h) – S(x) 
 
とすればいいだけなのが分かります。この直線PQの傾きを求めるには、xが1増えるとyがいくつ増えるかを求めればよいので、yの増加量をxの増加量で割ればよくて

44.jpg

です。結局やっていることが一緒なので出てくる式の形も全く一緒です。あとは幅hを0に近づければいいのですが、相変わらずすぐに代入するとゼロ除算形式になるので

45.jpg

と表記しておきます。これが f'(x)+g'(x) になることを示せばいいですね。よく考えると S(x) は f(x) と g(x) の足し算だったので

42.jpg

を代入して

46.jpg

まで分かります。少しずるい気もしますが、答えありきで考えるなら、ここから f(x) に関係する部分、g(x) に関係する部分に分けて

47.jpg

となっているのが分かります。明らかに1つめの項はf(x)の平均変化率、2つめの項はg(x)の平均変化率です。ここで極限記号の1つの性質ということになりますが、足し算になっているものの極限はそれぞれの項の極限に分離できます。つまり、上の式は

48.jpg

と書くことができます。右の項も左の項も足し算なので、それぞれは独立した項になっています。なので、両方の項の不定形を同時に解消した後でhを0にしなくても、別に左だけ単独で不定形を解消してhを0にして、右で同様のことをし、それらを足したとしても同じことになります。以上から、

49.jpg

が分かりました。S(x) とは

42.jpg

だったので、結局

41.jpg

が示せたことになります。

微分の諸法則 [2] – スカラー倍の微分法則

 もう1つ楽するために絶対知っておくと得する法則があります。それがスカラー倍の法則です。これは名前負けしていて、単に適当な定数 a があるとき、

50.jpg

を微分すると

51.jpg

になるという法則で、直感的なものです。そもそも何に役に立つのかということですが、たとえば、 「3x3」という関数を微分したいとしましょう。
そうすると、あの導関数の定義にあてはめると常に係数の3がついてまわって計算が面倒です。なので、「x3」の変数の部分だけ微分して、最後に係数の3をかければそれでいいですというのがこの法則の真意です。結局和の微分と同じで楽するための重要な道具です。
 これも同じように示してみましょう。ただ、これは定義にあてはめればすぐ分かるので示すとかいうほどのことでもありません。例によってわかりやすくするため上の塊を S(x) とでもすると今度は

52.jpg

ですね。そしてその導関数はやはり

45.jpg

と書けます。あとは戻すだけです。

53.jpg

 このように簡単な法則でも確認するようにしておくと、後に捗ります。ここで当たり前のように定数倍aを極限記号の外に出しましたが、これはそもそも極限記号で操作対象にしているhに全く絡んでいないのでこのように外に出しても問題がないのです。もちろんhは操作に絡んでいるのでhをlimの前に出すなどという謎の行為に及んではいけません。
 以上から

51.jpg

が示されます。
 
 よく初めての人は間違えやすいのですが、これは定数だからできることであって、変数は分離できません。つまり、 xの二乗の微分は、(x)’ × (x)’ ではありません。変数(ここでは微分対象の「x」のこと)をかけ算方式でこのように分離した場合は、少し違う法則に従います。これも微分の計算では常連なので次に紹介します。
 ちなみに、この和の法則とスカラー倍の法則を満たす計算は線形性があるといって、この法則があると非常に有り難がられます。統一的にどんなパターンでも適用でき、今まで見てきたように「公式」などという便利なものがどんどん作り出せているのはこの線形性という名前の一種の秩序があるおかげです。

微分の諸法則 [3] – 積の微分法則

定数倍なら分離してから微分していいことがわかりましたが、変数×変数になっていたらあの面倒な分数を一から計算しないといけないのでしょうか。実は違います。変数×変数は一般に積の微分法則と呼ばれている重要な法則があります。

54.jpg

これがその法則です。先ほどの和の法則、スカラー倍の法則と並び、この3兄弟は理解した上で覚えることが絶対に必要です。数学は暗記じゃないとは言いますが、2乗の展開の公式や因数分解の公式を暗記していなければ全く計算が進まず支障が出るのと同じでこれも覚えないと今後の微分の取り扱いに時間がかかりすぎて支障が出ます。この3つを覚えると、xの2乗から後は全くあの分数を当てはめなくても結果がわかるようになるほか、今後複雑な関数を計算するときにも常に役に立つのです。
 これも規則的で面白そうな見た目の法則だと思いますが、成り立つことを微分の定義にあてはめて確認します。この手の計算はこつが分からないと楽しくなく、そのコツ(なぜそんな変形を行ったかなど)はいろいろ分かっていないと言葉では説明しづらいのは確かです。わからなかったら理解は後からついてくるはずなので別に結果だけ覚えてもいいです。とにかく結果は覚えてください。
 これを確かめることは、これまでと全く一緒で適当に

55.jpg

とします。

45.jpg

を求めればいいわけですので、早速 S(x) を戻します。

56.jpg

S(x+h)というのは「S(x)という関数に x+h を入れた時の値」、つまり、単に x を x+h に書き換えればいいだけなのです。したがって S(x+h) = f(x+h)g(x+h) です。 S(3) なら S(3) = f(3)g(3) だし S(a+b)なら S(a+b)=f(a+b)g(a+b) の意味です。
 要するにこれを2つの項に分離して、片方はf'(x)g(x)、もう片方をf(x)g'(x)に持って行きたいのです。そこで一つ次のような細工をします。

57.jpg

ここで怖じ気づく必要はありません。なぜそのような発想に至るかは非常に説明しづらく、いくつか数をこなすと感覚的に分かるようになるレベルのことなのであまり気にせず進めます。この「式を有利に変形するためにある項を勝手に出現させ、埋め合わせとしてそのマイナス項を隣に付け足す」というのはよく使われる手法なのでそういう手法があるとだけ知っておけばよいでしょう。要するに、「あとこの項があればうまくいくのに・・・」という項があったら、勝手に出現させます。そしてもちろん勝手に足しっぱなしでは元の式と同じにならないので、隣にその項にマイナスをつけた「埋め合わせ」を用意します。そして埋め合わせじゃない方を使ってうまく式をつなげて、計算を進めていくということです。
 この場合、なぜこのように出現させると有利に進められるかは続きを見ていけば理解できます。ここで、次のように分離してやります。

58.jpg

一番目の f(x+h)g(x+h) と三番目の -f(x)g(x+h) を g(x+h) で因数分解して分離したのが左、二番目の f(x)g(x+h) と四番目の -f(x)g(x) を f(x) で因数分解して分離したのが右です。見ての通り、左はこのままhを0に近づける作業をすればf(x)の微分に、右はg(x)の微分になります。
 簡単なのは右の項です。右の項はこのまま h を 0 に近づける作業をすればそのまま f(x)g'(x) に一致することが分かるでしょう。左の項ですが、これは少し考えないといけません。よく考えるとこの微分の定義ともいえる変な分数は何をやっているかというと、h をそのまま 0 にしたら分母が 0 になるので、うまく分子を計算して分母が消えるようになってから h を 0 にしようという意味でした。この分数でそれをやった結果を微分と呼んでいるのです。
 そしてもう少し考えてみると、f(x)の微分になるようくくったこの項にくっついている g(x+h) は、別に今の段階で h を 0 にしても何の問題もありません。そもそも後から h を 0 にするというのは、今すぐに h を 0 にしたいのに分母に h がいるからできなかったというのが理由です。分離した結果である右の g(x+h) には分母がない、つまり 0 にしてはまずい h もいないので、今すぐ 0 にして、それから 0 になってはまずい残りの部分を計算した後でそっちの h を 0 にするというやり方でも問題ないはずなのです。
 つまり、簡単な例で言えば (p+1)(p+2) というのがあって、この p を 0 にしたいとき、別に「(p+1)の p を 0 にしたもの」(=1)と「(p+2) の p を 0 にしたもの」(=2)をそれぞれ求めて、そのかけ算(1×2=2)を「(p+1)(p+2) の p を 0 にしたもの」(0+1)(0+2)=2 といっても同じではないかということです。実際これは正しいことを言っています。したがってここでは右の g(x+h) は先に 0 にしても問題がないということを結論にします。ただし「はず」と書いたように、これは厳密な証明ではありません。このことを厳密に証明するにはこの「わずかな幅」とか「0に近づける」とかいう数学らしからぬあいまいな用語をもっと厳密に定義する必要があります。しかしながら、その段階まで掘り下げると完全に脱線し、しかも初めてだと哲学をやっているかのような気分になると思われるのでここでは直感が正しいということにしておきます。
 
 以上から、左の項は g(x+h) だけ分母に 0 にしてはいけない h がいないので先に 0 にできます。

59.jpg

あとは、このまま分子を整理してから h を 0 に近づけるという計算を行えば、左の項は f'(x) の微分に g(x) がかかったもの、右の項は g(x) の微分に f(x) がかかったものとみることができるので、

54.jpg

が成立します。究極的にはこの結果だけ知っておけば十分です。

冪関数の微分法則

ここからが本題です。 x の2乗、3乗・・・となると、x+h の 2乗、3乗・・・が出てくるということであり、あまりにも面倒でやる気が起きません。そこで、2乗以降はこれまでの法則を使って楽しましょう。

60.jpg

これの微分ですが、 xの2乗は xかけるx と書けることに注目してください。つまり、これを微分するというのは、あの面倒な分数を計算してもいいのですが、

61.jpg

を計算してもよいということになります。「・」は「×」と同じ意味です。かけ算は後先関係ないのでこれは結局

62.jpg

ということが分かります。要するに、あの変な分数を頑張らなくても、xの微分がいくつかさえ分かればいいということになります。あとはそれに2xをかけるだけです。しかも、これはすでに知っています。既にあの分数で計算した結果、y=ax+b という一次関数を微分すると y’ = a になることがわかっているからです。一次関数の切片がなく b=0 で、傾きが a=1 のときはこの一次関数はy=xと等しくなります。y’=a=1 ということになるため、xを微分すると1というのが理解できます。(x)’ = 1 を使うことで、x2 の微分は

63.jpg

であることがわかりました。
 次に 3 乗だったらどうなるか確かめましょう。(x)’ と (x2)’ なら結果が分かっていて、それぞれ 1 と 2x です。またまた積の法則にあてはめることであの変な分数を使わなくても計算ができてしまいます。

64.jpg

そろそろ何か法則があることに気づくかもしれません。4乗をやればほぼ確実に気づくでしょう。

65.jpg

このように、x の n 乗を微分したものは、右上の方の数字が1下がり、そして下がる前の数字が前に出てきます。証明はこのまま永遠に続ければ成立することが明らかです(厳密にはこういう場合帰納法というのを使いますが、省略します)。
 結局、n が自然数(1,2,3,4,5・・・)の場合には

66.jpg

が示されました。実際には n が自然数でないどころか分数や小数、負の数でもこの法則が成り立ちます。ただそれをやり出すと1/2乗とか0.3乗とかマイナス8乗とかいうのはそもそもどんな関数なのかという議論になってきて違う単元の話題が始まるためここでは自然数の時だけの議論にします。

接線の方程式

 いよいよ核心に迫ってきます。
 接線の傾きはどんな関数でもいいので関数 f(x) を微分した導関数 f'(x) を求めれば、任意の点の傾きとして使えるのでした。つまり、接線の「傾き」を求める方法は既に知っていることになります。ではもう1つの「切片」はどう決めるのでしょうか。

67.jpg

これが図です。適当な x=a における接線を求めたいとします。x=aにおける接線の傾きはf'(a) でただちに求まります。したがって接線の方程式は

68.jpg

まで簡単に分かりました。f'(a)というのは「f(x)を微分したもの f'(x) の x に a を代入した時の値」という意味でただの係数です。たとえば y=f(x)=xの2乗 なら、 f'(x)=2x ですが、x=2の時の接線の傾きを知りたいなら f'(2)=4 ということで傾き4です。このとき接線の方程式は y=4x+b というところまで分かった、という話です。
 切片は、そもそも傾きとは何かということを考えれば分かります。もう元の関数はいらないので問題に集中できるよう図を

69.jpg

としましょう。このときどうやったら切片 b を確定できるでしょう。それはこう考えます。直線の「傾き」とは右方向(x方向)に1進んだ時、上方向(y方向)に直線がいくつ進むかという意味でした。それならば、xがaだけ進めば、yは傾きのa倍だけ進むはずです。したがって、傾きはf'(a) なので、このa倍だけ進みます。よってx=0の時切片bをとりますが、x=aの地点では a・f'(a) だけ上に値が増えているはずです。

70.jpg

こういう感じです。スタート地点(x=0)の浮きは b だったので、x=aの場所では af'(a) だけ増えたことがわかります。したがって、x=a での y 座標は、増加量 af'(a) に最初の浮きを足して

71.jpg

でなければなりません。そして、この点のy座標は関数に直接値を代入することですでに f(a) と分かっているので、これが f(a) と一致しなければならないことから

72.jpg

となります。これより、b について解けば、切片が

73.jpg

と定まります。したがって、x=aにおける関数f(x)の接線は 

74.jpg

であることがわかりました。以上から、コンピュータに f(x) と f'(x) さえ与えてやれば、この式に従って接線を作ることで、ニュートン法が実行できるということになります。
 f'(a) は接線の傾きですから問題ないでしょう。右についている f(a)-af'(a) という切片ですが、これは別に覚えるとか難しいとかいったたぐいのことではありません。まさに「数学は暗記ではない」の正しい部分です。この部分は次のようにして理解してください。
 x=aの時のy座標は、接線なので関数がx=aの時の値f(a)と一致するはずです。つまり接線はこの (a, f(a)) という点は通ってなくてはなりません。

75.jpg

一方、切片というのはx=0の時の浮きです。直線における傾きとは、xが1増えるとyがどれだけ減るかなので、この場合傾きが f'(a) だから x が1増えるとyはf'(a)増えます。逆に言うとxが1減る(左にいく)とyはf'(a)減ります(下に下がります)。この直線は x=a のとき y=f(a) という点にいるのだから、x=0 の点に戻ると、この点から左にaだけ行くことになります。ということはa戻ったのだから傾き f'(a) の a 倍値が下がらなければなりません。以上から f(a)-af'(a) が直に切片ということになります

76.jpg

これで分かるのではないかと思います。したがってこれは別に覚えなくてもいいです。微分というのは接線の傾きとさえ知っていれば、x=a で f(a) を通るため、切片というのはx=0のときどれだけ浮いているかということですので「xをa戻した時の減少量であるaf'(a)を元居たy座標の f(a) から差し引いて f(a) – af'(a)」と思い出すだけです。式を覚える必要は全くなくて、単に直線のことを分かっているかどうかというだけの問題になります。
 
 以上で微分の原理はすべてです。

ニュートン法の公式

ついに接線の方程式を扱えるようになったため、ニュートン法の実践に入ります。何度も確認になりますが、ニュートン法はまず答えが求めたい数になるような方程式を立て、 (左辺) = 0 という形式にする必要があります。ルート2なら

3.jpg

です。こうなる理由は「左辺が0になるかどうかで答えに近づいていることが確認できる」からです。「左辺 = 0」という式なので当然左辺が0になれば答えです。そのような答えに確実に近づくための手法はいくつかあるのですが、その1つがニュートン法なのです。
 

6.jpg

これが y=(左辺) の関数だとします。関数上に適当な点を取ります。これはどんなに離れていても問題ありませんが、当然見込みが立つのであれば近い方が楽です。

7.jpg

次に、この点の接線を引きます。そこからまた接線がx軸と交わる点のx座標を求め、垂線を引いて関数に達した場所を次の点にし、これを延々と繰り返します。

8.jpg

9.jpg

接線の方程式を使えるようになったので、早速接線の方程式を作りながらルート2に近づけていきましょう。
 先ほど書いた手順を1つ1つやってもいいですが、計算量を減らすために少しだけ工夫をします。ニュートン法で出てくる各x座標をx0, x1, x2・・・としましょう。最初に打った適当な点のx座標をx0、そこから接線を引いてx軸との交点になった場所をx1・・・ということです。
 最初に、x0の点の接線を求めましょう。先ほどの公式のようなもののaがx0になっただけなので

77.jpg

ですぐ分かります。もちろんxは変数、x0は適当にとった定数(1とか10とか-3など)なので変数xとは無関係であり、区別する必要があります。
 次に、この直線のx軸との交点のx座標がx1になるのでした。x軸は y=0 のことなので、yを0にしたときのx座標がx1です。したがって

78.jpg

ということになります。あとはx1について解けばいいだけです。まずx1についてる邪魔な f'(x0) を割りましょう。

79.jpg

なにやら綺麗な形になります。移行すれば

80.jpg

が分かりました。これは何を言っているのかというと、別に接線の方程式を求めてx軸の交点を・・・ということをやらなくても、直接上のものを計算すればx1が出てくるということを言っています。x1での接線の方程式もやはり

81.jpg

という同じ形になるので、x2も同じ方法で得られます。結局、一般の場合についてこれは成立するため、n番目(n=0,1,2,3・・・)のx座標に対して

82.jpg

で次を求めることができます。結局、やればいいことはこれだけです。これこそがニュートン法の虎の巻ともいえる公式なのです。
 

ニュートン法の実践

3.jpg

の左辺をf(x)とすると、f'(x) = 2x なのがすぐわかります。とりあえず x0=2 とでもしておきましょう。各xが、現時点でのルート2の推定値ということになります。最初はルート2とは2のことではないかと仮定したわけです。
 公式に入れるために f(x0), f'(x0) を計算します。それぞれ x=x0=2 を代入するだけで簡単です。 f(2)=2, f'(2)=4 であるため、

83.jpg

が分かります。電卓のやることを手でやりたいといっているのに電卓をつかうのはおかしいかもしれませんが、ここからは電卓を使った方が無難です(もちろん手でもできますが大変面倒です)。
 全く同様に次を求めると

84.jpg

なので、

85.jpg

となります。すでにここで 1.41 が現れていることに注目してください。
 次も面倒ですが全く同様です。

86.jpg

最初に書きましたが f(x) というのは (左辺) = 0 の左辺のことです。つまり、f(x) が 0 に近いほど答えに近づいています。 f(x) は式が2乗されているので直接の誤差ではありませんが、言ってみれば今答えからどれだけ誤差があるの目安です。3回目の計算にしてすでにそれが 0.006944 などという細かさになっているところに注目してください。

87.jpg

この辺でやめておきますが、既に見知った値がかなりの精度で登場しています。このようにして計算を繰り返すことで、ルートが算出できるのです。
 ちなみに、値は慎重に決める必要はなく、たとえば最初の座標を99にしましょう。コンピュータに全く同じ手順をやらせてみると

88.jpg

これでもあっという間に打ち止め(コンピュータで表現できる細かさの限界まで高い精度になったという意味)になります。

微分方程式[6] – 偏微分方程式の解法

参考文献: フーリエ解析 (マグロウヒル大学演習), Partial Differential Equations for Scientists and Engineers (Dover Books on Mathematics)

今回は偏微分方程式についてです。このブログの中に偏微分方程式というシリーズは存在しますが、ここでは常微分方程式のうち面倒なタイプの方程式が必要な理由を説明するための道具に過ぎないため、可能な限り簡単にまとめ、ここで改めて書いていくことにします。
 ベッセル方程式やルジャンドル方程式といった常微分方程式を考える際、それ単体で考えてもいまいち重要性が分かりません。そもそもどこでどうやって出てくるのか、要するに何に使うかが分からないからです。そこでまず、そういった方程式はなぜ必要なのかを考えます。それを考える際最もお手軽なのが偏微分方程式を解く途中で出てくるという紹介の仕方です。しかしながら、偏微分方程式の解法の流れを知らなければ、その導出もできません。この記事では、そもそも偏微分方程式とはどうやって解かれるものかというところから紹介します。
 ここでは一切偏微分方程式の知識を前提としませんので、分類から典型的なタイプの解法までを1つの記事でまとめて紹介します。したがってかなり量があります。

偏微分方程式とは

 これまで、変数が 1 つの関数の微分項を含む方程式を取り扱ってきましたが、これらは特に常微分方程式 (ordinary differential equation) といいます。例えば変数を x 、関数を y(x) とすれば、y’ (dy/dx)、y”, … を含む方程式が常微分方程式です。
 一方で、変数が 2 つ以上の関数の微分項を含む方程式は偏微分方程式 (partial differential equation) といいます。とりあえず大量の変数の場合を例にとっても分からないので、一般論を述べる場合は常に 2 変数の場合とします。特に決まりがあるわけではありませんが、抽象的な場面(一般論など)において、常微分方程式の変数は x、解は y(x) とされることが多い一方で、偏微分方程式においては変数が x, y、関数は u(x,y) とされることが多いです。以下、特に「○変数関数」と明示しない限り、u というのは常に u(x,y) を意味するものとします。

線形と非線形

 名付け方は偏微分方程式とほとんど同じです。u(x,y) またはその微分の 1 乗のみを含む場合、線形 (linear) といい、そうでない場合は非線形 (nonlinear) と呼ばれます。つまり、

002.png

こういった項のみを含む場合、線形ということです。一方で

3.png

など、こちらは枚挙に暇がありませんが、u とその微分に定数倍以外の操作を加えたものを含む場合非線形になります。これから取り扱うものは常微分方程式同様全て線形です。線形でないものには統一的な解法が存在しないうえ、初歩的な現象は全て線形だからです。

常微分方程式との解の違い

 常微分方程式と偏微分方程式で決定的に違う点、初めての場合特に意識するべき点が 1 つあります。それは「任意関係」、常微分方程式で表れたそれは「任意定数」だったのですが、偏微分方程式では場合により「任意関数」になる点です。
 具体例を見てみます。常微分方程式で最も簡単な場合は、直に積分できるパターンです。

4.png

これは高校の教科書でもおまけとして載っていることがある、という事実からも分かるように、そのまま積分してよいパターンです。左辺を積分すれば y になり、右辺を積分すれば x になりますが、定数は微分すると 0 です。つまり一回微分して定数になったということは、それによって消えた定数もあるはずです。しかし、この方程式からはその定数は何であるかまでは分かりません。言い換えると、定数があったことは確かですが、明示されていない以上それは何でもよいのです。これを任意定数と呼び、C で表しました。結局解として

5.png

です。確かに両辺を微分すると定数である C は消えるので

4.png

に戻ることが分かります。この C は 100 でも 0 でも 1/334 でも何でもよいということですね。
 次に、もう少し発展した形を再確認します。

6.png

これも直に微分することが可能です。他に y やその微分がないからです。

7.png

微分すると定数は消えるので、やはり任意定数 C が出現しますが、もう一回積分ができます。もう一回積分すると

8.png

ですね。このとき新しい任意定数 D が出現しています。ここは C としてはいけません。なぜなら最初に出た任意定数と次の積分で出た任意定数には何の従属的関係もないからです。どちらも独立である以上、文字は変えましょう。このことからわかるように、一般に常微分方程式は微分階数と同じだけの任意定数が解に出現します。
 さて、偏微分方程式の場合も同様でしょうか。やはり常微分方程式同様、直に積分できるパターンは存在します。ただ、同じ一回方程式の簡単な場合でも、

9.png

のようなものは積分するだけでは解けないので注意が必要です。例えば左の項を x で積分すると、左は u になりますが、右は ∫Uydx になり、おかしなことになってきます。反面、x と y の微分が混合しない場合で、常微分方程式同様複数の階数が混在しない場合、つまり

10.png

のようなものは解けます。見た目通り y で微分されているため、 y で積分すればよいだけです。

11.png

見てください。このように任意定数 C のかわりに出てきたのは任意関数 f(x) です。これが解です。u は 2 変数関数なので、y だけでなく x も変数として含んでいます。つまり y だけで積分したということは、逆にいうと y だけで微分、つまり y で偏微分した場合、y がない項は消滅するということを意味します。例えば x だとか ルートx も y で偏微分すれば y に全く関係ないので 0 になるというわけです。以上より、任意定数だけでなく x のみの関数も y で微分すると全て消滅することがわかります。これが f(x) とおかなければならない理由です。
 全く同様に 2 階微分の場合も見てみます。

12.png

これもやはり x だけで積分してよいパターンです。

13.png

x で微分しているので、任意関数としては f(y) が出てくること以外は、何も問題がないでしょう。まだ積分可能です。

14.png

このように、x にとって y は全く独立で、変数と同じ扱いです。そのため f(y) を x だけで積分すれば f(y) x と、係数のような扱いになります。そしてまた違う任意関数 g(y) も登場します。やはり偏微分方程式でも、階数と同じだけの任意関係を含んだものが解になります。しかし、その任意関係は、複数の変数がありそれを偏微分している以上、微分している変数と違う変数を含む任意関係、つまり任意関数でなければならないという違いがあります。ただし、今後の解法上、これらは結果的に任意定数になる場合が圧倒的多数です。このように一般論としては任意関数になることを知っておくとよいでしょう。
 なお、このときに出てきた任意関係を含む解は一般解 (general solution) と呼ばれます。例えば先ほどの

11.png

は一般解です。この任意関係を具体的に指定したものは特解 (particular solution) と呼ばれます。上の一般解に例えば f(y)=y+2 を入れた

15.png

は特解です。

2階偏微分方程式の分類

ここからどんどん具体的な話に入っていきます。実用上非常に重要なのは常微分方程式同様 2 階線形偏微分方程式です。常微分方程式は 1 階の方程式もそれなりに出ましたが、偏微分方程式ではほぼ 2 階しか出てきません。そしてその 2 階線形偏微分方程式は、標準形が少し面倒な形になります。

16.png

なぜなら、常微分方程式なら 2 階、1 階、 0 階微分だけで済んだのですが、偏微分方程式は 2 つ変数があるせいで、x, y の 2,1,0 階微分、それに x, y で 1 階ずつの微分も出てくるからです。ここで滑らかな関数、つまり微分積分の授業で扱った都合の良い関数のみを取り扱うため、xy 微分と yx 微分は常に同じものとします。やはり常微分方程式と同じく、G=0 であれば同次方程式、G が定数含め 0 でなければ非同次方程式に分類されます。
 この A-G は全て x, y の任意の関数です。これも常微分方程式とは少し違うところなのですが、この A から G までの設定により、解き方や解の振る舞いが異なってきます。ちょうど 2 次曲線の分類と同じになっているのですが、

17.png

という、判別式のようなものを考え、これが 0 未満の場合楕円型 (elliptic)、0 の場合放物型(parabolic)、0 より大きい場合双曲型(hyperbolic) の方程式だと言われます。これは純粋に分類の問題なので、別に忘れてもよいのですが、分類上はそうういう風になっていることを知っておくことが重要です。そして物理で出てくる代表的な方程式は、これら 3 種類を見事に網羅しています。今回はその全てを取り扱います。解き方は全く一緒ですが、解の形がそれぞれ違います。解にどう違いが出てくるかに注目してみて下さい。

初期条件と境界条件

 このようにして分類がなされた偏微分方程式ですが、一般解は応用上ほとんど意味をなしません。具体的な状況設定に合う解を得てこそ、意味があるのです。その状況設定のことを初期条件(Initial Condition: I.C.)、境界条件(Boundary Condition: B.C.) と呼びます。これらをもとに、その状況設定にあう解を見つける問題を境界値問題といいます。上で得られた一般解をもとに、それらの条件を適用して解を得るというわけです。
 一般に、多くの偏微分方程式は時間を変数として含みます。つまり 1 次元であっても時間があれば 2 変数関数で、偏微分方程式になることを意味します。時間を変数として含む場合、t=0 (かそれ以外でも) における値のことを初期条件と呼んでいます。そして、空間領域を表す変数の具体的な情報を境界条件と呼んでいます。ここではこれらは区別しますが、一般に全て境界条件と呼ばれることがあります。

変数分離法

先ほど非常に簡単なタイプの偏微分方程式は実際に解きましたが、常微分方程式同様、1 階微分や 2 解微分が混在した場合はこの方法ではお手上げです。定数係数線形常微分方程式ならば、解を

18.png

とおくことで方程式がただの代数方程式(1次方程式、2次方程式)に変換され、いとも簡単に一般解が分かりました。偏微分方程式でも、すごく複雑な形になりますが、似たような置き方で解くことは可能です。しかし任意関数は一般に境界条件に適用し辛いので、そのような方法が採られることはまれです。つまりこの手の手法は偏微分方程式の場合不向きです。
 物理の問題のように、境界条件が決まっている偏微分方程式(厳密にはその条件にも縛りがあるので、境界条件さえあれば100%そうなるというわけでもないですが)の場合は、統一的な解法があります。それが変数分離法と呼ばれる手法です。これはなぜそう置くとうまくいくのかを探るのは無駄なので、とにかく現状このように置いて矛盾は見つかっていないという、ある意味熱力第二法則的な理解をしておくとよいかもしれません。今までの流れだとこのまま線形偏微分方程式の訳の分からない理論が展開されると思ったかも知れませんが、実用的な部分において、理論的とはいえないかもしれないものの、偏微分方程式の解法というものはむしろ常微分方程式より明快なものです。とにかく、まずは次のように置いてみて下さい。 u(x,y) なら

19.png

です。こう置くと、定数係数の線形偏微分方程式は、

20.png

という形に持って行けます。こうなると、左は x のみ、右は y のみになるので、左右は全く独立であるにもかかわらず、同じ値でなければならないということを言っているということになります。それは、両辺が定数でなければ起こりえないことです。したがって

21.png

とおくことができます。これにより

22.png

という 2 本の常微分方程式に分離されるわけです。それぞれの方程式を解き、X(x) と Y(y) を確定すれば、元の方程式の解は

19.png

というかたちで求まるわけです。色々な方程式の種類によって、どのタイプの常微分方程式に分離されるかが変わってきます。特に多次元の場合で、極座標など、違う座標で考えると定数係数ではない線形常微分方程式が出現することがあります。簡単に言うとベッセル方程式やルジャンドル方程式はそういった違う座標系で考えた場合に出てくる常微分方程式であるために重要です。

放物型: 1次元熱伝導方程式の導出

 実際に 3 つの偏微分方程式をみてみます。分からない場合は式だけ覚えてもいいです。まずは放物型偏微分方程式の代表選手である熱伝導方程式 (拡散方程式)です。1 次元熱伝導方程式の場合、厚みなどが存在しない針金のような棒を考え、時間とともにその棒の各点の温度を表す関数 u(x,t) を求める問題になります。別名からも分かるように、物質が拡散していく現象もこの方程式で記述されます。その場合はその点における密度として u(x,t) が使われます。
 方程式は 1 次元の場合、

23.png

で表現されます。偏微分方程式の分類は、2 階の係数を問題にしているわけですが、ここでは B にあたる xt 微分が存在せず、しかも C にあたる t の 2 階微分がいないので、判別式のようなものは 0 になります。したがって放物型になることがわかります。
 この方程式の導出は比較的シンプルです。3 次元で考えてもさほど変わらないので、ここでは u=u(x,y,z,t) だとしてこれを導出します。これは熱の出入りを表す方程式なので、まず熱の流束というものを考える必要がありますが、一つの事実 (法則) から出発します。それがフーリエの法則と呼ばれるもので、ある微小な面から別の面への熱の出入り、つまり熱の流束は

24.png

で表されるというものです。K は熱伝導度と呼ばれる係数で、要するに熱は高い方から低い方に向かってその勾配に比例して流れていくことを表しています。u を温度分布だとすれば、∇u は温度の高い方への勾配を取っていることになりますが、マイナスをつけることで熱が高い方から低い方に流れるという本来の ∇ とは逆方向であることを表現できるわけです。∇u が大きければ熱の変化がより大きいということになります。 -K∇u が熱の流れのベクトル場になっているということですね。
 次にある体積 V とその表面 S を考えます。この領域における熱の出入りは、流束の面積分を取ればいいわけです。ここで領域から出ていく流束 = 領域を細かく区切った各体積から流出する流束の正味量になるので、ガウスの発散定理を適用し

25.png

となることがわかります(マイナスがないので正確にはこれは熱の「入り」に関する方程式になっているといえますが、マイナスは両辺につきますからあってもなくても一緒なのでここでは省略してあります)。偏微分方程式に手を付ける段階でベクトル解析に未着手という方はそれほどいないと思うので、ここではベクトル解析関連の説明は最小限にしています。用語が分からない場合、かなり長編ですがここから確認して下さい
 ここで、

26.png

とします。あくまで流束、つまり外向きに出て行こうとしている量は「マイナスの」K∇u です。そもそも勾配は増加方向を向いているベクトルだからですね。この Qin(t) は、ある時刻 t における熱の流入量を表しています。流入量と流出量は裏返しの関係なので、それにマイナスがついたことでプラス K∇u になっています。なぜ発散定理を使って体積分表記にしたかは理由があります。3次元なので、熱量の総量も体積分によって表すことができます。例えば質量の密度が ρ(r) だったなら、その体積分で質量が表せるように、3 次元空間なので、任意の体積 V 内に存在する熱量は体積分で表記できる、というわけです。考え方もほぼ質量と同じです。これも物理的な定義になるのですが、熱量は比熱を c、質量を m 、温度が T のとき

27.png

で与えられます。比熱は1度上昇のために必要な熱量を意味していて、定数とは限りませんが、手計算でやるような問題は大体定数です。体積分で表記する時は微小体積 dV を使うので、この質量の部分は例によって密度 ρ(r) とあわせて ρdV と表記され、結局熱量の合計は

28.png

と表記できることがわかります。この式は位置によって積分されているので時間のみの関数 Q(t) です。また勾配の時と似たややこしい議論の繰り返しになりますが、dQ/dt は総熱量の変化量で、増加する向きの量を意味します。したがって、この dQ/dt はその時間に入ってくる熱量を意味します。これは先ほどの Qin(t) と一致しなければなりません。この手の立式は連続の方程式と呼ばれていて、電磁気学など他の分野でも全く同じパターンの式の立て方が出てくるので、楽するために理解をしておきましょう。ベクトル解析を使わないでも導出できますが、そうすると計算が複雑になります。以上から

29.png

ここでなぜ発散定理を使ったかがはっきりしたのではないでしょうか。体積分が = でつながったため、かっこの中を等しいとしますが、右辺で温度 u 以外は時間に依存していないとすることで、∂u/∂t と積分の中に入れることが可能です。

30.png

このような式が導かれました。ここで注目すべきなのが、K は一般に定数ではないということです。伝導率は密度同様に一様とは限りません。これは位置の変数になり得るので、そうなると外側の∇がきいてきます。このように偏微分方程式の導出の際には、∇・(K∇u) といった形の表記がしばしば出現します。この後紹介する波動方程式も同様の表記が出てきます。もし一様だと仮定すれば非常に簡単に表記できて、

31.png

と表記できることが分かります。これが一般の座標系における熱伝導方程式です。もし1次元であれば、ただちに

23.png

が得られます。導出方法は一回分かれば忘れても問題ありません。この方程式は至る所で出てくるので結果の式だけ知っておくと便利です。

双曲型: 1次元波動方程式の導出

波動方程式は文字通り伝搬する波を方程式にした結果得られるものです。関数y(x,t)に対して、次のような形をしています。別にu(x,t)でもいいですがここでは2次元平面における「高さ」の意味があるためyにしました。

9.png

それこそ波が関わる多くの現象を元に導出が可能なのですが、一番特別な知識を必要としないと思われる方法で今回は導出します。それが弦の振動です。これは運動方程式で導出できます。簡単のため弦のパラメータは全部定数ということにします。

01.png

まず、図としてこのようなものを書ける必要があります。弦の振動は、変位をyとし、位置xと時間tの2変数関数です。したがってy(x,t)が求めるべき関数、というのが第一歩ですね。
次の一歩として、弦をモデル化します。弦を弾くと上下に揺れるのですが、変位はまばらです。そこで図のように細かく区切ります。太いのがその区切りです。
そうして区切られた弦の細分には、両端に同じだけの張力Tが働きます。隣の区切りでも同じようなバランスで張力がかかっているので、大きな視点で見れば結果的には糸の内側の張力同士は実質打ち消され、両端に等しく張力がかかっているように扱えます。また、変位はまばらであるため、左右の小片も何らかの異なった変位を持ってつながっているはずです。したがってかなり大げさに図のように書きました。角度も違うので、それぞれの角度をθ1、θ2としています。
 ここからちょっとしたトリックを使っていきます。物理の世界ではおなじみですが、近似という手法のことです。その前に変位yについての方程式を作りたいので、なんとか方程式を立てましょう。まず思い浮かぶのは運動方程式でしょう。yを従属変数としたいので、y方向の力の釣り合いで運動方程式を立てればよいことがわかります。力ですが、図より Tsinθ がy方向の張力です。したがって以下の方程式が作れることがわかります。

2.png

時間微分ですが、変位yは2変数関数なので記号が偏微分となります。mはこの小片の質量を意味しています。しかし、これは微小区間で立てた方程式ですので、そのままmとして進めるわけにはいきません。mを弦固有の定数や今取っている細分のパラメータで言い換える必要があります。具体的な置き方ですが、この弦の密度(線密度)をλとおくことで、長さΔsとかけあわせ

3.png

とします。次に困る点は角度です。角度も同様にその瞬間その瞬間、しかも小片の場所ごとにバラバラのはずなので、同じようにθを含む項を何か図の他のもので表現する必要があります。
そこで近似の登場です。ここで1つ条件を付けるわけですが、次のように設定します。「弦の振動は十分小さい」(θはほぼ0に近い)ということです。確かに弦を弾いても、目に見えて飛び跳ねるものを見たことはありません。こうなると何をしたいのか分かると思いますが、sinθをθに置き換えることができるようになります。物理でも、振り子の時勝手に角度を小さいとして同じことをやっていたかと思いますので、詳しくはその辺を思い出して下さい。覚えていない場合は、次のように考えると早いでしょう。sinθをテイラー展開して、θを微小とすると、第一次のθ以外は全部高次微小量で消滅するという、という考えです。以上から、運動方程式は次のところまで変形できることがわかります。

4.png

力学を全くやっていないとこの辺の近似のやり方が理不尽に見えるかもしれません。そう見える場合は単振り子などでこの手の近似に慣れておくことをお勧めします。次に、sinは外れても角度は残ったままなので、これを弦の小片のパラメータで置き換えます。角度θは非常に小さいため、sinθだけでなくtanθとも等しくなります。テイラー展開によりsinθ≒θとできることは分かりました。同等に、cosθをテイラー展開して全く同じことを適用すると1になります。つまり、tanθはsinθ/cosθ=θ/1=θという近似が効くわけです。以上よりθが小さい時

5.png

が成り立ちます。こうするともう簡単です。なぜならタンジェントとは傾きのことであり、それはつまりx方向に少し進んだ時のyの増加量を意味するので、この場合は∂y/∂xのことです。tanθ1とは、x座標が図のxの場所の時の∂y/∂xを意味します。tanθ2は、x+Δxの座標にいる時の∂y/∂xのことです。したがって

6.png

が成り立ちます。この辺りまで来ると目的を見失っているかもしれませんが、波動方程式で足りないのはあとyのx2階偏微分です。この式はあとΔxで割って極限を取ればxの2階偏微分になることに注目をしてください。微分方程式を導出するというのは、ある関数f(x)があったとして、

7.png

をどうにかしてひねり出すことが目標なのです。そこを念頭に置けばこうやって変形してきた経緯も理解できると思います。以上より

8.png

までは導出できたことにしましょう。あとは両辺適当に割れば、それらしいものが出てくることが自然と分かります。

10.png

T/λはどっちも定数というお約束にしたのでこれをaの2乗とすれば終了です(2乗とおかなくても別に問題ないですが、実用的な形にしたときルートが登場するため、そうならないように波動方程式のここは2乗になっています)。最後の欠けたピースはΔsです。これがΔxになれば、微分の定義となるのでΔx→0のもとで波動方程式が導出されます。実はこれはΔxとおけます。角度が十分小さいと仮定したため、図は分かりやすいようあからさまに角度を付けてありますが、ほぼ角度がないも同然だからです。微小とはそういうことです。したがって角度が小さいという仮定のもとではΔsは曲がっておらず直線も同然なのでΔxとしてよくなります。この辺の近似の嵐はインチキに見えるかもしれませんが、慣れるしかありません。熱伝導方程式同様、受け入れがたい場合は結果だけ覚えても良いです。以上より

9.png

が得られました。2次元以上の空間だと熱伝導方程式同様、空間微分がラプラシアンに変化します。

楕円型: 1次元ラプラス方程式の導出

ラプラス方程式とは、次のような方程式を言います。

11.png

これは楕円型方程式の代表選手です。そして導出は一番簡単です。なぜなら上記方程式で時間変化がないとしたとき(定常状態)の状況を表す方程式だからです。例えば2次元だったら

12.png

が導出すべき方程式になります。熱伝導方程式は

31.png

と表すことができました。ここで2次元の場合は

13.png

と書けますが、定常状態ではどうでしょう。定常状態とは、時間が十分に経過(t→∞)し、それ以上時間が変化しなくなった状況のことを意味します。つまり時間微分しても時間変化しないため0となり、関数も時間依存してu=u(x,y,t)だったものが時間に依存しなくなりu=u(x,y)となることを意味します。以上から、定常状態では

12.png

ということになります。熱伝導方程式に当てはめれば、十分時間が経った後の温度分布を示した方程式だと言えます。

有限区間1次元熱伝導方程式の解法

実際偏微分方程式とはどう解いていったらいいものなのでしょうか。ここでは上の代表選手3人に最も単純な場合の境界条件を付け、それぞれ変数分離法を適用し、解が得られることを確認しましょう。まずは1次元の熱伝導方程式

23.png

からです。変数分離法の手法とは、かなり勝手ですが解である関数をその独立変数のみの関数の積で表示できると仮定して、常微分方程式複数個に分離させる手法でした。矛盾が見つかっていないため正しいということですが、本当にそう置いて正当な解が出てくるのか確認です。その際には、どうしても境界条件の設定が必要になってきます(そうでないと解を絞り込めない)ので、ここでは次の境界条件を設定します。

27.png

棒の長さは有限(長さL)で、両端の温度が0だということを意味しています。本当は温度は0でない場合でも解けるのですが、変数分離法は同次線形境界条件と呼ばれる、読んで字のごとく上のように右辺が0になるようなタイプでないと適用できません。つまり0でない場合は同次線形境界条件に変換するための細工が必要になります。そこを追求するとかなりスペースを使う上に些細な問題であるのでここでは省略し、常に境界条件は右辺0とします。まずは基本から掴むことにしましょう。
 まず、u=u(x,t)であるため、xのみの関数X(x)とtのみの関数T(t)の積であることを仮定します。

14.png

実際に、元の方程式に代入しましょう。偏微分ですが、上のように分離しているため、x微分にとってtは定数なのでXのみ微分されます。Xはxのみの1変数関数なのでこのx微分は常微分に化けます。同様にtについても適用すればややこしい偏微分はいなくなるというわけです。まずそのまま代入して

15.png

になるのがわかります。次に偏微分は微分する変数以外に対しては効かないので、x微分におけるt、t微分におけるxは定数扱いされ

16.png

となることがわかります。
 次に、片方の辺はある変数のみ、もう片方はまた別の変数のみという風に変形できるはずです。実際、両辺をaXTで割れば

17.png

で、左辺はxのみ、右辺はtのみの関数で分離できていることがわかります(aは定数なので関係ない)。
割るのは別にaXTでなくてもいいと思うかもしれません。確かにそうですが、テクニックとしてこう割ることをお勧めします。この後の手順を進んでいけばよく分かると思うのですが、分子をこのように係数等を伴わない微分された関数のみにしておくと、常微分方程式に分離したとき綺麗な形になるからです。
 左辺はxのみ、右辺はtのみの関数になりました。それぞれ独立に動かせる変数なのに、これらが等しいというのは、両辺共に変数と等しくなければあり得ないことです。これが変数分離法の常套手段なので、必ず体感して覚えておくようにしましょう。したがって、その定数を次のようにおきます。

18.png

またここで馴染みのないやり方が登場しているので注意しましょう。今後そうでない場合も出てきますが、まずここは負の定数で置くのが基本です。しかも2乗を使って、マイナスかける定数の2乗とします。2乗にするのは意味があって、2階微分ということは2階常微分方程式が登場します。そうなると特性方程式が出てきます。2乗でなければルートが必須になってしまうことがわかります。また、2乗は必ずプラスですので、それにマイナスがつくことで常にマイナスであることを表記でき、わざわざ負の定数などと注意書きしなくても分かってもらえるので見た目上親切です。ここは0でもプラスでもいいではないかと思うかもしれませんが、実際に検証していけばマイナスしかあり得ないことが分かってきます。要するに定数部分はマイナスと置かないと、「物理的にあり得ない解」というものが出てくるのです。その辺についても注意しながら、とりあえず上の式のように置いて進めてみましょう。そうすると、次に

19.png

の2つの常微分方程式に分離できることが分かります。
 ここで、なぜ定数をマイナスλの2乗と置いたのかを調べてみましょう。上の方の方程式に注目してください。これは特性方程式が負になるパターンなので、解はcosとsinの和になることを既に知っていると思います。具体的に定数を正、0、負とした場合のX(x)の方程式を列挙します。

25.png

それぞれ、2階常微分方程式の定番パターンなので、詳しい解き方は書きません。[4]の問題が解けるのであれば、そのまま答えが出せるはずです。

26.png

それぞれの場合について検討していきましょう。定数が正、つまり一番上の解であった場合、境界条件を適用すると

28.png

となることがわかります。この解は一体どうなるのでしょうか。それは簡単です。多くの方がきらいと思われる線形代数の用語でいえば自明な解、つまりA1もA2も0となるしかありません。A1もA2を決定するための連立方程式だと見てください。クラメールの公式と呼ばれる手法があったと思いますが、その場合、右辺が0だと行列式も0になってしまうことがわかります。クラメールの公式が思い出せない場合でも、正直に上式からA1=-A2とすればA1=A2=0しかないのが分かると思います。X(x)=0ということは、u(x,t)=X(x)T(t)=0です。これは、偏微分方程式の解u自体が自明な解になることを意味し、物理的に意味を持たない解です。両端の温度が0だったとしても、真ん中が熱かったら少なくとも最初の時点は棒全体が0になるはずはないでしょう。したがって、定数を正と置くと正しい解が得られなくなるため、正とは置けません。
 0の場合も全く同様です。こちらはただの算数で、境界条件を同様に代入してただちにA1=A2=0が得られることがわかります。つまりこれも自明な解になってしまいます。やはりこの定数は0と置くこともできません。
 以上より、定数は正や0の場合、uが自明な解になるほかないため、負と置かなければならないということになります。そこで改めて

19.png

のセット、つまり定数がマイナスλの2乗という負の値であったときのもので考えます。このときは自明な解ではないものがちゃんと存在するので、順番に調べていきましょう。

29.png

正直に代入した結果、こうです。一見他のパターンに似ていますが、違います。確かにA1は0になるしかありません。そこで下側にその結果を適用すれば、最終的に

30.png

が得られることが分かります。これはA2は必ずしも0になる必要はないということに注目しましょう。なぜなら、A2が0にならなくても、sinの中身が0になれるからです。sinの中身が0、±π、±2π…の場合には、値が0になります。したがって、この定数λは完全に任意というわけではなく、

31.png

という制限が付くことが分かります。値が飛び飛びになるという事実に注目をしてください。また、変数分離法の最初の段階で、負の定数とおきましたが、λ自体は実数です。実数なら2乗してマイナスを付ければ必ずマイナスになります。負になるのはマイナスλの2乗全体としてであって、λ自体は負になっても問題ないということにも改めて注目しましょう。以上から、

32.png

という結果が得られました。これで、解が

33.png

に定まりました。
 次にT(t)の方程式を見ます。これはtの1階常微分方程式に過ぎません。解をeの累乗と仮定して代入すれば解けます(Xの方程式では省略しましたが、それと全く同じ手法です)。指数関数は微分しても形が変わらないため、両辺に同じ指数関数が残り、両辺をその指数関数で割ることで取り除けて、ただの代数方程式が得られます。

20.png

とおいて、pを求めたいわけです。これを代入すると

21.png

という方程式になりますが、累乗は両辺共通なので取り除いて

22.png

を得ます。これより、任意定数をつけて

23.png

であることがわかりました。ここでλは既に定まっているため代入して

34.png

が得られます。これで解は

35.png

と定まります。任意定数はAとBの積をCとおきなおしました。上記境界条件のもとではこれが答えです。このうえに初期条件が加わることで、さらに任意定数Cが定まることになりますが、少なくともこの時点で分かることとして、熱伝導方程式は時間によって指数関数的に0に近づいていくというのが挙げられます。次の項を見ていけば分かりますが、波動方程式ではこのような減衰項は現れません。
 これが偏微分方程式の解き方の流れです。手順は決まり切っていて、変数分離法で常微分方程式に分離させ、その時おいた定数を得られた常微分方程式と境界条件を考慮することによって限定します。それがSturm-Liouville問題と呼ばれる微分方程式の特徴です。この問題の場合、微分方程式の階数と同じだけの任意定数とは別に、今回のλのような定数が混じった方程式になりますが、境界条件を当てはめることでλが必ず離散的な値として限定できます。Sturm-Liouville問題には

36.png

というややこしい定義がありますが、このサイトでこれから考える、偏微分方程式を変数分離して得られる2階の常微分方程式は全てこの問題です。

有限区間1次元波動方程式の解法

今度は波動方程式にも挑戦します。先に書きましたが、こちらは熱伝導方程式と違って減衰する要素(指数関数)が登場しないという違いがあります。それは時間微分が2階微分になっていることによるものです。やはり同次線形境界条件の設定が必要になるので、熱伝導方程式同様、有限区間ということにして問題に挑みましょう。

9.png

37.png

波動方程式は弦の振動から導出できました。その見方でいくと、両端が0という条件は、いわゆる「固定端」という条件に相当します。
 最初に、求める関数y(x,t)をX(x)とT(t)の積とします。

38.png

元の方程式に代入します。

39.png

偏微分に関係ない方の変数は定数のように前に出せるので

40.png

次に (xのみの式)=(tのみの式) を作り上げ、右辺左辺で変数を分離させます。この際、常微分方程式になることを見越して最高階数(ここではX”とT”)に係数がつかないようにしておくことが鉄則です。要するに、それによって後で得られる常微分方程式が標準形(見やすく、解きやすい形)になります。

41.png

左辺はxのみ、右辺はtのみの式です。それぞれ独立に動かせる独立変数なので、それが等しいと言うことは両辺は定数に他なりません。そこで両辺をマイナスλの2乗(負の数)とします。λは実数です。これは熱伝導方程式の時と全く一緒で、負でなければなりません。理由は全く同じで、式も境界条件も先ほどと全く一緒なので検証するのは省略します。

42.png

以上から、偏微分方程式は2つの常微分方程式に変換されます。

43.png

境界条件がないとλに縛りを入れられませんので、ここでは境界条件を付けたxから手を付けていきます。見て分かると思いますが熱伝導方程式と全く一緒な式で、境界条件も同じものです。X(x)の微分方程式の一般解は最も典型的な形なので暗記していると思いますが

44.png

です。境界条件を適用しましょう。といっても先ほどと一緒です。

29.png

これより、

30.png

が得られることが分かります。やはり自明な解以外のものが存在し、それはsinが0になるときで

31.png

という制限が付くことが分かります。2度目になりますが、負になるのはマイナスλの2乗全体としてであって、λ自体は負になっても問題ないので、こうする必要があります。λが定まったので

32.png

という結果が得られ、X(x)の方程式の解は

33.png

です。
 一方でT(t)の方程式もX(x)の方程式と全く同じ形をしています。aの2乗がくっついてるだけなので、そのままλxとなっていたところをaλtとして終わりです。

45.png

λは既に定まっているため、次のように直すことができます。

46.png

これでX(x)T(t)が定まったので

47.png

を得ますが、任意定数の積もまた任意定数なので、AはBと統合してまとめることができ

48.png

が答えです。ここでは特に初期条件(t=0の弦の形や、はじき方による初速)を指定していないため、任意定数が残っています。

1次元ラプラス方程式の解法

2次元方程式の解法

今回のまとめ

A1. 1次元熱伝導方程式(1次元より上の場合x微分がラプラシアンに)

23.png

A2. 1次元波動方程式(1次元より上の場合x微分がラプラシアンに)

9.png

A3. 2次元ラプラス方程式(一般にはラプラシアン=0の形)

12.png

微分方程式[5] – ガンマ関数/ベータ関数とその演習問題

参考文献: フーリエ解析 (マグロウヒル大学演習)

参考文献: フーリエ解析 (マグロウヒル大学演習)

前回までで線形上微分方程式のうち最も簡単な定数係数線形常微分方程式を習得しましたが、ここからは少し発展したタイプの変数係数線形常微分方程式というものに挑戦します。ただしこれらは統一的に解けるわけではなく、決まった形に対して決まった解法が知られていて、しかも特定の変数係数を持っているものは物理の問題を解く際出てくるので詳しく調べられており名前がついている、といった程度のものです。
 その前に特殊関数(special function)というものを知っている必要があります。簡単にいうと、式は複雑だがとても便利なのでそれ自体を一つの記号として定義した関数のことで、これは今後調べていく変数係数の常微分方程式を解いたとき、その解のなかに出現したり、あるいはその方程式の解そのものが記号として定義(=特殊関数として定義)されたりします。
 今回は、そういった方程式を解く前に、特殊関数への入り口ともいえるガンマ関数、ベータ関数を紹介します。ここから特殊関数というものの扱いに徐々に慣れていきましょう。ここでは厳密さは全部無視し、道具として使うための方法だけを紹介します。変な記号を使って収束がどうだの定義がどうのこうのと議論することが目的ではなく、実用上の問題を解けるようになることが重要です。

ガンマ関数の定義

特殊関数を知る上で一番基本的なのがガンマ関数(gamma function)と呼ばれる関数です。基本的に特殊関数の定義はどれも見た目が複雑です。複雑ですが他の特殊関数は基本細かい定義は覚える必要がなく、性質だけ押さえておけば十分であるにもかかわらず、ガンマ関数は定義を覚えておく必要があります。なぜならガンマ関数は積分計算にも応用でき、その形を知っていないとガンマ関数に帰着させることができないからです。
 ガンマ関数は以下のように定義されています。

1.png

xの累乗に収束性の優れた e の累乗をかけて無限大まで積分した関数です。無限大の極限では ∞/∞ の不定形になるため、ロピタルの定理より上下を何度も微分すれば下は永遠に形が変わらないので最終的に分子の次数が落とされ続けて 0 に落ち着くことがわかります。ガンマ関数のかっこの中は n なのに x の肩の数字は n-1 であることに注意してください。

階乗としてのガンマ関数

 重要なのは、この変な関数は階乗の拡張だということです。別名階乗関数とも呼ばれます。では階乗となぜ結びつけられるかを調べます。ガンマ関数に限らず、特殊関数はそれぞれ特有の性質を持っており、元のややこしい定義式をいじり回すというよりはそのいくつかの性質を使ってうまく処理してやる、というのが主なやり方です。そのなかでもガンマ関数は次の性質が特徴的です。これは非常に重要なので絶対に覚えましょう。

2.png

証明方法自体は原則どの特殊関数であっても定義式をいじるだけなので本質的ではありません。結果としてこのような性質があることを把握し、運用できることが重要です。ただしいきなり与えられても運用する気にはなれないというのも事実です。そこでひとつひとつ理由を調べていきましょう。この性質は定義式に従うだけで容易に証明可能です。

3.png

このように、隣についている自然対数は微分しても形が変化しないので、部分積分を適用します。こうすれば x の累乗は肩の数字が落とされて n-1 になったものを見いだせることがわかります。

4.png

先ほど書いたように、 x の累乗と自然対数では、ロピタルの定理により x が何乗であっても必ず e の方が増加スピードが速いため、最初の項は 0 になり性質の証明が完了します。
 ガンマ関数のかっこの中が n の時、その数字を1減らす代わりに前に減らした後の数字が出せるということは、これを延々と繰り返せば n(n-1)(n-2)・・・・ となっていくことがわかります。これは階乗に非常に近いものですが、どこまで小さくできるのでしょうか。基本的にガンマ関数は 0 以上の数に対して定義されます(0を下回ると正の数の場合とはまた違った様子を示します。これは後述)。したがってかっこの中が 1 以下になった場合そこで終了です。その Γ(n) の値がいくつになるかは、整数か非整数かによって区別されます。一般に非整数の場合は手計算では値を求めることはできません。ただし整数の場合は結果が知られていて、というよりは手計算で調べることができて、

5.png

です。以上より、n が整数であれば必ず Γ(1) まで落ちてくるので、ガンマ関数整数の場合、階乗そのものであることがわかります。

6.png

このように、ややこしい定義ですが、Γ(n+1) で n! になることに注意して下さい。あいまいになった場合で、参考書などを参照できない環境にいる時は、ガンマ関数の定義を覚えていない場合は詰みですが、覚えていた場合はこのように定義からどれも簡単に導出できるためお試し下さい。

非整数のガンマ関数 – 階乗の拡張

ガンマ関数が階乗の拡張だと言われるのは、ガンマ関数が整数の場合、今まで知ってきた階乗の性質を全て満たしている上、さらに整数でない場合の階乗というものも考えることができるからです。既に書いたように

1.png

で、x が整数でないと部分積分などしても x は永遠に消滅しないため手計算では値を知ることができなくなります。ただし半整数、つまり (2n+1)/2 (もっと簡単に書くと 1/2, 3/2, 5/2・・・・)の時は手計算で値を算出可能です。これは実はガウス積分になるのです。私は勝手にルートπ積分と呼んでいますが、

040.png

という積分のことです。実はこれはちょっと手を加えてやると二重積分、極座標変換可能な形に持って行くことができ、極座標変換数すると簡単に積分できるようになるため値が求まるのですが、詳しい方法は既にやったためそこを参照して下さい。なぜこのような形にできるかは、具体的に代入すればわかります。

7.png

このようにルート x (xの1/2乗) 自体を新たに x と変数変換することで、ガウス積分に帰着しているのです。したがって Γ(1/2) = ルートπ の関係がわかりました。これより、たとえば Γ(7/2) を知りたければ

8.png

になるわけです。ガンマ関数の計算の感覚はこれでつかめてきたかと思います。結局、このように数字を1ずつ落としていくことで、最終的に n の部分は [0,1] 区間まで落ちます。その Γ(n) がいくつかが問題なのです。上は 1 または 1/2 まで最終的に落ちるので具体的に値を示せましたが、例えば Γ(1/3) などはすぐには分かりません。こういった場合、それ以上詳しく値を求めるのはコンピュータの仕事です。

負のガンマ関数

正のみ、と書きましたがガンマ関数の n は実数であれば何でも入れることが許されます。ただし n が正の時とは異なり、負の場合ガンマ関数の挙動は大きく異なります。n が 0 含む負の整数のときは値が発散するのです。あまり出てこないので簡単な確認にとどめておきますが、これはガンマ関数の性質を使うと分かります。

2.png

という性質によって n の数字を下げられるということですが、逆に考えると、これは数字を上げられることも意味します。つまりこれを変形して

9.png

だということです。 n が正の数であればどんどん上げていけるのでおかしなことは起こりませんが、0 のときや負の整数時は必ず上をたどっていくと(0の場合であればすぐに)分母に最終的に 0 が登場して発散してしまうことがわかります。しかも正の無限大に発散するか負の無限大に発散するかは左または右どちらから近づいたかによって異なってしまいます。これは知識として知っておくだけで当面問題は無いでしょう。

ベータ関数の定義

ガンマ関数の発展形と呼んで良いのかは分かりませんが、亜種のような存在としてベータ関数というものがあります。ベータ関数は今後の展開から言うともう出てくることがないのですが、特定のタイプの積分計算を非常に簡単にしてくれます。またベータ関数はガンマ関数のみで定義することもできます。なにかと新しい記号が登場して混乱してしまいがちですが、とにかく本質的な部分は、特有の性質がありそれを駆使すれば変な定義式を直接計算せずに結果が出せるということです。
 ベータ関数は以下のように定義されます。

10.png

ガンマ関数と似ているような、似ていないような定義ですが、積分範囲が0から1になっていること、また m,n の2変数関数であることに注意してください。ガンマ関数は次回から紹介する予定のベッセル関数の定義式に含まれているため重要ですが、x(1-x) というのは、ある確率 x とそうでない確率 (1-x) の積なわけです。積分範囲0から1もそう関連づけると覚えられるのではないかと思います。そのため確率などの分野では重要ですが、基本的な微分方程式を見る上ではもう出てこないので必要に応じて覚えたり忘れたりしてください。何度も書きますが重要なのは定義式を丸暗記することではなくてその特殊関数にどんな性質があり、その性質を活かしてどう扱えばよいのかのノウハウの部分です。定義式は無理に暗記する必要もなくて、参考書が片手にあれば済むことなのです。

三角関数によるベータ関数の表現

あの変な定義式は、三角関数によって表示できます。

12.png

これも覚える必要はありません。三角関数の半周期積分と結びついていることを知るのが重要です。私たちはベータ関数の出発点を

10.png

としました。どのようにしてこの三角関数の表記が得られるかを証明しましょう。
 とりあえず x の m-1 乗が欲しいのですが、 sinθ は 2m-1 乗と書いてあります。 2m とあるので sinθ の2乗を x というふうに変数変換すれば x の m 乗になることがわかります。そうすれば dθ の兼ね合いで m-1 乗になってくれるかもしれません。まずはそういうシンプルな考えで、

13.png

とおきます。別に cosθ の方を x と置いてもいいですが、sinθ より cosθ の方がマイナスが出てくるぶん微分するとややこしくなるのは経験済みでしょう。こうすると積分範囲は 0 → 1 になることがわかります。ベータ関数の定義と同じ範囲です。あとは dx に変換するためこの sinθcosθ という断片を 2m-1 乗、2n-1 乗の方から1個ずつ抜き取ればいいわけです。そうすれば次のように変形できます。

14.png

あとは変数変換を適用しましょう。すると自然とこれがベータ関数と等価であることが導かれます。

15.png

これは変数変換を適用しただけの状態です。 sinθ の二乗が x であるため、sinθ そのものはそのルート、1/2 乗であることに注意が必要です。同様に cosθ はルート 1 引くサイン二乗なので、1-x にルートがかかったものと表記できます。これらは肩の数字が 2m-2, 2n-2, つまり偶数なので 2 を分配することで綺麗にすることができて

16.png

と、証明が完了します。こちらから出発して sin, cos の式を導くことも当然できますが、興味がある方は挑戦してみて下さい。とにかく、これでベータ関数は 2 つの形で定義できることが分かりました。最後に書きますがこの表記は特定のタイプの積分で威力を発揮します。その威力を確かめるにあたっては、次に紹介するある重要な性質を知っておく必要があります。

交換法則

ベータ関数は交換法則が成立します。すなわち

28.png

です。これは定義式で簡単に確認できます。

10.png

において、1-x = y と変数変換してみましょう。すると積分範囲 0→1 は 1→0 になり、 dx= -dy で変換できることがわかります。つまりこれは積分範囲を逆にして 0→1 に戻せばマイナスが出るので、実質的にマイナスも何も出ないことを意味します。被積分関数が書き換わるというだけですね。

29.png

ベータ関数とガンマ関数の関係

以下の関係式を知らなければ、ベータ関数のことを知っているとは言えません。それくらい重要な関係式がこちらです。

11.png

このようにベータ関数はガンマ関数を使って表現できます。つまりあの変な定義式をいじらなくてもガンマ関数だけでベータ関数の値が分かるというわけです(ガンマ関数そのものが変な式と言われてはおしまいですが)。
 これも三角関数表記同様、自明とは言いがたいので確認をしてみましょう。定義式から導出が可能ですが、右辺はガンマ関数が3つもあって、このまま戻すと分母分子に積分が見えてややこしいので次のように方針を立てます。まずΓ(m)Γ(n)をガンマ関数の定義式から展開して、Γ(m+n)B(m,n) に一致することを確かめます。というわけで、

17.png

がスタート地点です。これはただガンマ関数の定義式を並べただけなので何も怖じ気づくところではありません。もちろん Γ(m) と Γ(n) は完全に独立なので、累次積分のような形式にすることも可能です。誤解を生まないため変数は独立だという意味で x, y と違うものを選んであります。ただこれだけでは進めようもないので、一つ、工夫をします。それが前回ガンマ関数が半整数の時に行った計算と全く同じ手順ですが、ルートx を新たな変数とするような変数変換です。なぜそんな変換をするのかですが、先ほど書いた様に実際に累次積分形式にしてみると分かるかもしれません。

18.png

このままでは進めませんが、e の累乗の x+y は見知った形です。これがもし x の二乗 + y の二乗であれば、極座標形式に変換することでさらなる式の変形ができます。ガウス積分でさんざん経験してきたやり方でしょう。実際そのようにするためにはルート x、ルート y をそれぞれ新しく x, y と置き直せばいいだけなのです。とりあえずガンマ関数に適用するとどうなるか、試してみましょう。ここでは誤解が起きないよう変数変換後の形を知るためだけのガンマ関数 Γ(k) を考えます。積分変数も z としておきます。

19.png

ここで z の 1/2 乗を 新しい変数 w とおきます。

20.png

積分範囲はルートになっても 0 から ∞ で変わることはありません。

21.png

結果、ガンマ関数はこのように書いてもよいことがわかりました。ガウス積分と関係しそうな形ですね。実際、Γ(1/2)にすればこれはガウス積分そのものであることがわかります。最後、変数変換をして wdw で w が一つ出てきましたが、それは w の 2k-2 乗に吸収されて 2k-1 になったことに注意が必要です。
 以上を Γ(m)Γ(n) に適用しましょう。変数を置き直すと文字が一杯出てきて大変なので x, y のまま書き直します。

22.png

これで、極座標に変換できることがわかります。極座標変換とは、 2 次元の場合は

23.png

と置くことを意味します。大体微分積分でやった訳の分からない公式はこういうところで突然出現するのですが、なぜこうなるか分からない場合のために詳しく書いておくと、確かに微分積分では意味不明な変換の公式がありました。クラシアンだかトレビアンかヤコビアンとかいう名前のよくわからない行列式を使って変換する方法です。実用上はあんなものは覚える必要は無く、次のように理解して下さい。
 基本にあるのは直交座標、xy 座標の「dxdy」という長方形(正方形も含めて)を変換したい座標系でならどう書けるか、というところだけなのです。xy 座標は軸が固定なので dxdy は横方向、縦方向の微小な移動距離の積として、微小面積を意味するものとして定義できました。ただ極座標は r 軸、θ 軸というのは常に直交しますが、場所によって向きが違うことが問題です。こういった中で dxdy に相当する長方形を作り出す必要があります。変数変換において常に大事なのは「曲がりは限界まで拡大すればないのと同じ」だということです。

24.png

極座標とはこういう座標の見方を言います。この青く塗りつぶした dS = dxdy に相当するものを求めればいいわけです。既に書いた様に曲がりは極限においてはどうでもいい(直線に等しい)ため、高さ dr、下の辺は rdθ ですが、上の辺も rdθ の長方形として扱えることがわかります。これはかなり大きく書いたために明らかに上と下の辺の長さは等しくないですが、無限まで拡大するともはや直角な長方形として扱うことができます。これが極座標などの変数変換を手際よく取り扱えるようになるためのポイントです。
 下の辺が rdθ なのはいいとしても、上は絶対に rdθ ではなく (r+dr)dθ のはずだ、と思う人は 2 次の微小量という言葉を思い出して下さい。別に (r+dr)dθ としても結果は一緒なのです。曲がりは無視していいので馬鹿正直にこれが上底、下底 rdθ, (r+dr)dθ で高さ dr の台形だとしましょう。小学生以来かもしれませんが (上底+下底)×高さ÷2 というのがありました。上底+下底は rdθ + (r+dr)dθ = 2rdθ + rdrdθ です。これに高さ÷2 をかければ rdrdθ + rdrdθdθ/2 になります。2 項目は微小の度合いが一項目よりも更に小さいわけで、こんなものはあってもなくても同じです。したがってこれは省略でき、いずれにせよ rdrdθ という同じ結果が得られます。そういう理屈だったら上底はどうせ消えるのだから最初から dr など含めないで下底と同じでいい、というのが合理的ですね。
 以上から dS=rdrdθ であることがわかります。これによって極座標と xy 座標の間で

23.png

という関係式が導かれるのです。3次元だと球座標や円筒座標というものもありますが、それも全く同じイメージで理解できます。あれは変な行列式をいじりまわして得るものではありません
 かなり遠回りしましたが、この極座標変換を施すことで

25.png

であることがわかります。積分範囲は注意です。無限まで行くので r は 0 から無限でいいのですが、角度は違います。元々 x, y ともに 0 から無限大までということは、これは x,y 軸で区切った 4 つのエリアの内、右上の区間全体でしか積分しないということです。半径はどこまでも伸ばせますが角度は 0 から 90 度までしか回せません。そのためこのように変換されます。以上より、 θ と r が完全に分離できているので積分の積に直すと

26.png

です。こうなればもう終わったも同然で、左のほうはガンマ関数の定義そのもの (先ほどの z の 1/2 乗を w とおいたタイプの方です) であって、右はベータ関数の三角関数表示形そのものだからです。どちらも前に 2 が要りますが、左端にある 4 を 1 個ずつ分配すれば済むことです。したがって

27.png

が示されます。ここで sin, cos が本来定義したものと逆なので B(n,m) = B(m,n) としていることに注意して下さい。逆に言えば m,n か n,m か、つまり cos が先か sin が先かは大した問題ではないということです。両辺を割れば

11.png

の関係を確かめることができました。

ベータ関数/ガンマ関数の相反公式

もう一つ、特にベータ関数を扱う上で計算テクニックとして知っておく必要のある公式があります。これは相反公式と呼ばれるもので、

33.png

です。p が 1 未満というあたりいかにも確率の計算に使いそうですが、要するにベータ関数の m, n があわせて 1 になる場合このような公式が使えるということです。計算が劇的に楽になるほか、既知の Γ(1), Γ(1/2) 以外でもこれに当てはまれば手計算で出せる場合が増えるということになります。
 これを示すのは実は複素解析の知識が必要です。ここでは複素積分のことは前提としていませんから、興味があれば複素解析の問題として挑戦してみてください。より正確には、複素解析の知識によれば、以下のことが知られています。

34.png

とりあえずこれは鵜呑みにするとして、この積分が相反公式が利用できる場合のベータ関数に等しいところまでは確かめましょう。これもやり方が決まっていて本当にノウハウといった感じになってくるのですが、次のように変数変換します。

35.png

こうすると、x が 0 のとき y は 0、x が無限大のときは x/(1+x) が ∞/∞ の不定形なので上下 x で割って極限をとり y が 1 になることがわかります。こうおくとちょうどベータ関数の積分範囲になるよううまくできているわけです。

36.png

このようにちょっとしたテクニックが必要なので注意してください。ここで登場した 1/1+x ですが、これは

37.png

であることに注意が必要です。また変数変換するにあたって

38.png

であることも注意しましょう。以上を踏まえると、

39.png

と変形でき、ここで変数変換を実施すれば

40.png

の関係が示せました。以上から

33.png

です。

演習問題1. ベータ関数、ガンマ関数の計算方法

30.png

(1) ベータ関数には

11.png

の性質があることを思い出して下さい。またガンマ関数は

2.png

という便利な性質がありました。そして覚える必要があるのは Γ(1)=1 (自明) と Γ(1/2) = ルートΠ でした。これらのツールがあればたいていの問題は計算できます。特に n が整数の場合 Γ(n) はただの高校までで馴染んできた階乗 (n-1)! です。

31.png

(2) も同様に処理してください。

32.png

(3) は明らかに相反公式を使う問題です。

41.png

演習問題2. 三角関数の積分問題

42.png

この手の積分を高校時代に苦労して計算したことがあるのではないでしょうか。この手の計算は実はベータ関数の計算として処理できます。

12.png

にあてはめていきましょう。三角関数表示する場合は係数の 2 があることに注意が必要です。
(1) sin がないということは、 2m-1 = 0 なので m は 1/2 であることを意味します。同様に、cos が 6 乗ということは n が 7/2 であることを意味します。以上よりただちに

43.png

であることが分かります。倍角公式といったものを使ってごちゃごちゃやるよりは美しいと思います。
 (2) 全く同様にして、m=2, n=3/2 の場合として処理できます。

44.png

(3) 積分範囲に注意する必要がありますが、結局これは係数 4 を出すだけで 0 から π/2 積分に帰着できます。なぜならば、8 乗されているのでこれはマイナスの領域を持たないからです。 マイナスは偶数乗されることで全て上に跳ね上がります。結局同じ山が 2 個できるイメージになり、その山半分 4 個分に相当するのです。

45.png

図にすればこういうことです。以上より

46.png

が得られました。
 このように、ベータ関数の計算は公式とガンマ関数の性質を使って小学生の計算をひたすらやるだけです。この手の三角関数の積分はベータ関数として解釈すると非常に簡単に計算できるので、次から使ってみるとよいかもしれません。

今回のまとめ

A1.ガンマ関数の定義

1.png

A2. ガンマ関数の数を下げる公式

2.png

A3. nが整数の場合、高校までで習った階乗に一致

6.png

A4. Γ(1/2) の場合、ルートΠ
 
B1. ベータ関数の定義

10.png

B2. 三角関数によるベータ関数の定義(前にある2に注意)

12.png

B3. 交換法則

28.png

B4. ベータ関数とガンマ関数の関係

11.png

B5. 相反公式

33.png

ベクトル解析[7] – 直交曲線座標系

 ラプラシアンとは、以下のようなもののことでした。

001.png

覚えることは理解することとは違いますが、まあこれ自体は分かりやすい表記なので意思の有無にかかわらず覚えたことでしょう。忘れることもなさそうです。
 ナブラやラプラシアンといった記号の扱い方は、[2] の記事で紹介しました。この記事で紹介した範囲では、ナブラというものはとても簡単だったはずです。しかしながら、ある程度勉強を進めていくと、突然次のようなものに遭遇します。

002.png

あの覚えやすい記号だったはずのラプラシアンが、こんなにも意味の分からない表記に化けているのです。これは、先ほどのxyz 座標ではなく、極座標(球座標)におけるラプラシアンです。座標系が違うとラプラシアンやナブラの表記が変わるのです。
 これは一見複雑に見えますが、とあるルールに従って簡単に出すことができます。この記事では難しい議論を全面的に回避した上で、そのルールを探ることを目的とします。しかしながら、ベクトル解析の7番目の記事にしているだけあって、勾配・発散、また発散定理のことは前提とします。

直交曲線座標

 3次元空間を考えます。一般の座標系のため、 x, y, z とは限りません。そこで変数は q とおきます。ある微小距離 dl だけ移動した時、その表記が三平方の定理で表現される、つまり

003.png

と表現されるような座標系を、直交曲線座標と呼びます。
 xyz 座標はいずれの h も 1 で、曲線ではありませんが直交曲線座標系の一つといえます。要するに、その座標系における変数の単位ベクトルが全部直交しているような座標系ということです。全て直交していれば純粋に 3 方向のベクトル成分の足し合わせと見なすことができ、ちょうど上のような表記にできるのです。

極座標における微小量

 上の話は完全に一般論であり意味がよく分かりません。そこで、例を導入しましょう。極座標の場合を考えます。

021.png

r は直線的に変化するのですが、θ, φ は回転方向を向くのが分かります。r を半径とした円周をそれぞれ描いたとき、接線方向になるわけです。ちょっと図が良くないというか、変化量を大げさにしすぎたのでピンクのラインが直交してないように見えますが、これは直交します。このように極座標もその点その点で dr, dθ, dφ ベクトルの向きが変わっていくのですが、どの点でもこのような図を思い浮かべれば常に直交していることがわかります。
 しかし、この場合 xyz 座標と明確に違う部分があります。それは尺です。確かに xyz 座標と角度は違うものの同じような直交する3本の軸はその点で作れるのですが、それぞれのパラメータが変化する時の変化量が3つの軸では平等ではないのです。
 図を見ると、r 方向の変化量は今までの x, y, z の軸と同じ長さのスケールなので、r 方向へ dr だけ変化すれば、そのままこの方向への変化は dr で表現できます。ただし、角度は違います。角度というのはある意味倍率であり、実際の長さそのものではありません。つまり、r の大きさに応じて明らかに実際の変化量は変わってきます。弧の長さは角度を x とすると rx で表現されますから、r が大きいほど同じ角度の変化でも実際のずれは大きくなっていきます。したがって、角度が dθ 変化すれば実際には rdθ, φ 方向が dφ 変化すれば実際には rcosθdφ の移動があることになります。これは図より明らかです。今考えている位置ベクトルから、各パラメータが微小量変化したとき、実際にはどれだけの移動が起こるのかで考える、ここが重要です。
 したがって、直交する3方向それぞれにおいて、パラメータが微小に変化した場合、実際この3つの軸で起きる変化の量は

004.png

であることが分かります。ここで出てくる微小量の係数が h1, h2, h3 に対応するのです。dl や h は、極座標の場合、この直交する3方向で三平方の定理を適用して

005.png

であることが分かりました。この h はそれぞれの方向のいわば「倍率」です。3 つのパラメータが同じ尺でものを表していないために、このようなことが起こっているのです。
 これで、直交する 3 方向の微小変化量が求まったことになります。要するに、これが直角座標と同じように扱うために必要な微小変化量なのです。xyz 座標における dx, dy, dz と同じ役割を果たすのは、dr, dθ, dφ ではなく dr, rdθ, rcosθdφ なのです。これからの見通しを簡単に言うと、偏微分における dx, dy, dz がそのように置き換わるために、ナブラおよびラプラシアンの表記が変わってくるので、それを調べていこうということになります。

直交曲線座標におけるナブラ

 以上の話題から、直交曲線座標において、微小量を xyz 座標における dx, dy, dz のようにシンプルに (平等に) 扱えるようにしたい場合は、

006.png

という置き換えが必要になります。このように定義した ds を使えば、直交曲線座標における各種偏微分の扱いも、xyz 座標のようにとてもシンプルになります。これはこのような置き換えによって、それぞれが同じ尺になったからです。したがって、関数 u の勾配は

007.png

のように表現できます。一番右の式は純粋に ds = hdq に置き換えただけの関係式です。
 これより、一般の直交曲線座標におけるナブラの定義は

008.png

であることがわかりました。ここで注意したいのが、単位ベクトルの位置です。このサイトでナブラのことを知った人は、[2] の記事で定義した時に

001.png

と紹介したので、話が違うではないかと思うかもしれません。実は、偏微分の前にある方が正式な表記です。ただ、 xyz 座標では上のように偏微分の後に置くことも許されます。なぜならばxyz 座標における単位ベクトル i, j, k は定ベクトルだからです。
 前に置く方が正しいというのは例えば勾配の意味を考えれば分かります。 grad u は、それぞれの方向にどれだけ変化しているかを表す量です。どの軸でもいいのですが、その軸の変化量をとって、その軸の方向を向いていなければならないのです。一般の直交曲線座標は、極座標の例を見ても明らかなように、3 方向は直交しますが、点によって向きが全部違います。単位ベクトル自体も定ベクトルではなく関数なのです。したがって、偏微分の右に置くと単位ベクトル自体も偏微分するという意味になってしまい、本来のナブラの意味をなしません。「その方向の変化量をとって、その方向を向かせる」のが勾配です。

直交曲線座標における発散

 このナブラの定義を使えば、非常に単純にラプラシアンが計算できますが、その計算は大変煩雑です。今書いたように一般の直交曲線座標では単位ベクトル自体も定ベクトルではなくベクトル関数になっているからです。したがって、この定義を元に形式的に div grad u を求める方法は、おすすめできません。他に、偏微分の公式を使って導出する方法もあるのですが、いずれも機械的な計算ではあるものの、計算量大変多いので、実用的とは言えません。
 そこで、そういった機械的な計算ではなく、イメージ先行でどうにか計算を最小限で済ませる方法を考えます。ここで鍵を握るのが、発散です。以前、発散は

009.png

で表現できることを知りました。このことは既に [4] の記事で示されています。
 なお、この式は、暗記するようなものでは全くなく、これそのものが発散の定義です。発散というものは、その点で流れ(流束)は出ているのか入っているのかを表す計算だということを知っています。流束を集計するのは面積分です。それがある点で流れるか出るかを知りたければ、小さい体積を取って無限小に縮めてやればいいのです。まさにこのイメージを数式という言葉に翻訳しただけのことであり、上の式を覚えるにしても意味のない記号の羅列として覚えることはオススメできません。
 それでわかりにくかったら単に微小体積における発散定理だと思ってもいいです。体積 ΔV における発散定理を作り、体積が微小なことを利用して平均値の定理を使い体積分から ΔV を出してやった後両辺を ΔV で割れば上の式はすぐ得られます([6]の記事で勾配定理・回転定理に対して行ったことと全く同じ)。
 発散をわざわざこのような表記に直したのには、[4] の記事でも書きましたが、「座標系によらない表記にする」という重要な目的があります。この式は単に、「流束を集計して集計した閉曲面の体積で割り、極限を取る」と言っているだけです。この定義は極座標だろうが、一般の直交曲線座標だろうがそのまま成り立ちます。このため、方針としては、 [4] の最後でやったことと全く一緒です。今回は逆にこの発散の一般的な表現を前提として、任意の座標系における ΔV を作り、表面 S の面積分 f・ndS を計算し、 ΔV→0 の極限を取るのです。これで、直交曲線座標において発散がどう表現されるか簡単に知ることができます。

上の図を見て下さい。これからの議論は [4] の最後と全く一緒です。ただ dx, dy, dz を ds1, ds2, ds3 に置き換えて同じ議論を繰り返しているだけです。別に結果に影響しないのでどっちでもいいのですが、以前は直方体の中心を起点としたのに対して、今回は図のように置きます。
 既に書いたように、xyz 座標と同じように微小量を簡単に扱うためには ds=hdq としてそれぞれのパラメータによる尺の違いを平等にしてあげなければなりません。そこで、上の図では dq そのものではなく ds=hdq が三辺の長さに相当しているわけです。これより、例えば q1 方向に対しては、

010.png

であることが分かります。dx, dy, dz と同じように扱うには ds1, ds2, ds3 を使うことが必要です。したがって、微小面積 dS も、上の場合直角座標なら x 方向に対して dxdy としたところですが、一般の直交曲線座標ならば ds2ds3 に相当します。つまり dS = h2h2dq2dq3 でなければなりません。
 式を見て分かるように、h2h3E1 そのものを一つの関数と見れば、q1 に分母を付けることでそのまま微分になります。

011.png

このように 1= X/X を出現させて目的を達するといういつものパターンです。これで長々とした分数は微分になり

012.png

となります。
 あとの方向は数字を入れ替えるだけなので調べる必要は必要ありません。以上から、

013.png

であることがわかります。最後に体積 ΔV ですが、これも簡単です。ゼロ極限において xyz 座標では dV= dxdydz でしたから、一般の直交曲線座標においては dV = ds1ds2ds3 です。このことを使えば

014.png

であることがわかります。これが直交曲線座標における発散の表現です。一つも省略せず書いたため、時間がかかったかもしれませんが、イメージさえできれば途中の計算など必要ありません。

一般の直交曲線座標におけるラプラシアンの定義

 以上で道具が揃いました。

007.png

015.png

 これらを組み合わせれば、∇2u = div grad u が簡単に求まります。

016.png

これが一般的なラプラシアンの定義です。これに座標系ごとの h を代入すればすぐに各座標系におけるラプラシアンが分かります。文字にすると長かったですが、イメージさえ分かっていれば、これ自体はすぐ導出できます。
 これで、極座標におけるラプラシアンを求めるための準備が整いましたが、一般の直交曲線座標で一貫して話を進めてきたことからも分かるとおり、座標系に対応した h を求めてやれば、どの直交曲線座標でもこの公式が使えます。そこで、最終的な結果もより簡単になるため円筒座標から調べましょう。

円筒座標におけるラプラシアン

 円筒座標 (r, θ, z) の場合、図より

017.png

であることがすぐ分かります。r はそのまま進行方向を向きますし、z は定ベクトルです。結局尺を合わせなければならないのは θ だけであり、これは角度なので係数が r になることがすぐに分かります。あとは代入すればすぐです。

018.png

以上が、円筒座標におけるラプラシアンです。難しい計算が何もないことが分かります。使っているのは積の微分だけです。

極座標におけるラプラシアン

019.png

これもそのまま代入するだけ。積の微分しか使いません

020.png

これより、

002.png

が得られます。
 以上から、一見不規則に見えるこのラプラシアンの表記の変化の仕方も、「直交曲線座標系」というくくりで根源的な部分を見ていけば、結局一つの規則の下に成り立っているということが分かります。厳密な議論は何もしていないのでこれをもって「理解した」とはいえないかもしれませんが、とても対称的な「鋳型」からこれらの表記が得られることを知れば、少しは親しみも持てるのではないでしょうか。

まとめ – 最短の手順でラプラシアンを得る方法

step1. 尺の違いを考慮する
 
 任意の座標系を頭に思い浮かべ、さらに任意の点 P でそれぞれの方向の微小量が直交している図を思い浮かべます。例えば極座標だったら、半径はがそのまま伸びるのは結構ですが、他のパラメータは角度なので、微小量増えるとその増えた微少量そのものではなく、今の半径に比例した量だけ、実際の移動が生じます。これらの効果を考慮した 3 成分

022.png

を求めます。これこそが xyz 座標における dx, dy, dz に相当するものなのです。
 
step2. 発散を求める

009.png

 この式そのものがまさにイメージ通り、発散の定義です。これが座標系によらないことを利用して流束を計算します。
 ある点 P を起点とした微小な直方体を作り出します。どの方向も扱いが同じなので 1 方向だけ求めたらあとは数字の入れ替えですむことが分かります。そこで ds1 方向の出入りを計算し、dV = ds1ds2ds3 に注意すれば

012.png

が暗算で求まります。あとは数字を入れ替えた物を加えれば

015.png

が完成します。
 
step3. ラプラシアンの公式の完成

007.png

015.png

 を組み合わせ

016.png

が得られます。ここに実際の h を代入すればラプラシアンが得られます。あるいは最初から具体的な座標系を考えて h に代入した状態で計算しても同じです。

熱力学 [1] – 熱力学第一法則

準静的な過程

 熱力学的な変化の過程を経て、最終的にそれ以上変化しなくなる状態が考えられ、この状態を熱平衡状態と呼びますが、その時の状態状態によって定まる巨視的な量 (圧力、体積、温度など) を 状態量と呼びます。通常、物質の熱平衡状態は、温度 T, 体積 V, 物質量 n によって記述することができます。理想気体の状態方程式

004.png

というものがあったように、一般に気体の場合、R は定数なので、T, V, n を特定すれば圧力 P が特定されます。そういう意味で、P は T または V の代わりにすることもでき、(T, V, n) の他に (P, V, n) といった状態量の組み合わせでも、気体の状態を特定することができます。
 この、熱力学的な変化には、元に戻せる変化と戻せない変化があります。何らかの変化を加えると、最終的に熱平衡状態に行き着き、状態量が特定できますが、一般に熱力学的な変化の途中、つまり平衡状態に達するまでの非平衡な間は、複雑に変化が起きるので状態量が特定できません。言い換えれば、平衡状態では状態量が特定できるので、状態量どうしのグラフ (通常 T-V 図、P-V 図が用いられる) を作ったとしたら、平衡状態では点が打てますが、非平衡状態は点が打てない (どこにいるのか分からない)ということです。

005.png

そのため、点線で示したように、まっすぐなのか、曲線状なのか、それとも数式で表しづらそうな変な経路で熱平衡状態に達したのか、分かりません。これは困ります。
 ここで登場するのが、準静的過程という変化です。発想は簡単に言えば、「ある程度の時間が経てば熱平衡状態に達し、点が打てるというのであれば、少しずつ変化させてその都度熱平衡状態にさせ続ければ、連続的にグラフが書けるのではないか?」ということです。具体的には、変化の幅を微小に取ります。そして、分割したそれぞれの微小変化に対して、系が熱平衡状態に達するまでに必要な時間よりもさらに十分多くの時間をかけて到達するようにします。このようにすることで、状態量の変化過程をグラフに表すことが可能になります。

006.png

そして、準静的過程は幅が微小 (無限小) であるため、どちら向きに進むこともできます。つまり、準静的過程は A-B という変化も、 B-A という逆向きの変化も行うことができます。このような過程は逆向きに進めることから可逆であるといいます。しかし、一般の変化は、「極めてゆっくりと」変化させることができないものばかりであり、不可逆です。
 下図のように、A-B 間の変化を経た後、また別の変化を経て、元の A に戻ってくるような過程も考えられます (別に、熱平衡状態は A だけで点線のループを描いている図でも同じです)。

007.png

たとえば、蒸気機関は、水蒸気に熱を吸収・放出させることで膨張・収縮が起こり、それによって生まれた仕事を使って動いています。こういった場合、定常的に仕事をしてくれなければ困るので、上のように、定期的に同じ過程を繰り返し続けることが必要になります。このように、始点・終点の熱平衡状態が同一であるような一連の過程はサイクルと呼ばれます。実用上重要になってくるのがこのサイクルで、どれくらいの仕事を行えるのか、どれくらい熱のやりとりが起こるのか、など、そういったことを調べる必要が出てくるのですが、何しろ一般のサイクルは図のように点線 (不可逆過程のため途中に点が打てず分からない) のため、こういったサイクルを考える上で、本来変化は不可逆なものではありますが、可逆の準静的過程に理想化して置き換えることでそのサイクルの性質を調べるといったことが行われます。この意味で準静的過程は重要で、様々な準静的過程の性質、およびそれによって構成されたサイクルの性質を見ていくことが必要になります。

熱力学第一法則 [1] – 熱力学第一法則

 実際に準静的過程を見る前に、熱や仕事といった量を特定するための下準備が必要です。これを行うために大変重要であるのが、力学におけるエネルギー保存則のような存在である熱力学第一法則です。気体 物体) の温度が高いというのは、その気体を構成している分子の動きが激しい (エネルギーを多く持っている) と見ることができます。熱量、たとえば水をストーブの上に置くと、水の温度が上がります。これはストーブの熱によって、水の温度が上がった、つまりストーブがある一定の「熱量」を水に与えたことによって、温度が上昇した、つまりエネルギーが増加したとみることができます。同様に、理想気体には

004.png

という状態方程式があったように、たとえば、圧力 P が大きくなると、温度 T は増加します。ピストンを思い浮かべると想像がつきやすいのですが、圧力が大きくなると、体積は小さくなります。熱平衡状態から縮めるということは、何らかの手を加えなければなりません。熱平衡状態から自発的に縮んでいくことはないのです。これは、手で縮めることによって、圧力を大きくすることができます。力を加え、体積を縮めたので、ここには仕事が生じます。つまり、仕事によっても温度を上げる、エネルギーを増加させることができます。
 熱と仕事は別物であるかのように見えますが、このように温度を上げる (下げる) ことができるという点で等価性があります。手をこすれば暖かくなります。仕事によって摩擦を引き起こせば、それは熱になります。早い話が、温度を上げるための熱を与える手段は、直接高い熱源から熱を与える以外にも、仕事を与える方法があり、仕事と熱は等価なものであるということです。ただし、エネルギーという観点で見れば等価ではありますが、行き来が自由かというとそうとは限りません。仕事は摩擦など熱に 100% 変換することができますが、熱というのはエネルギーで最も質の低い形態ということもでき、熱は 100% 仕事に変換することができません (これを数式などで具体的に表現した物は熱力学第二法則と言います)。
 さて、ピストンの話で行くと、物体に仕事をすると、物体のエネルギーが増えます。ピストンの方の気体の視点で見れば、「仕事をされた」ので、エネルギーが増えました。逆に仕事を気体がすれば、エネルギーは減少します。これは好みの範囲なのでこの表現が嫌なら符号を逆にすればいいだけなのですが、一般に仕事は「する」方を正に取り、熱量は「吸収した」方を正に取ります。つまり、吸収した熱量 Q と、した仕事 W を用いれば、物体のエネルギー (ここでは U とする) の変化 ΔU は、

008.png

と表現できます。これが熱力学第一法則の数式的表現です。ここでいう Δ というのはその本来の意味、つまり「変化した量」という意味であり、微分積分における「微少量」とは必ずしも一致しないので、注意が必要です。そのため、紛らわしいのである状態 A から B に達したとき、ΔU と書かずに Ub-Ua と表現する書籍もあります。また、符号ですが、「ΔU=Q+W」と表現する書籍も存在します。これは、「された仕事」を正と取っていることによるもので、仕事をした場合 W<0 となり、やはり上の式と本質的には違うものではありません。この辺の符号の取り方は好みの問題であるので、何でもよいと思いますが、ここでは最も多く採用されている上の Q-W の表現をとります。また、この物体のエネルギー U のことを、内部エネルギーといいます。後述しますが、実は、この内部エネルギーは状態量です。つまり、ある熱平衡状態に対して (基準点を決めれば) 一意的に定まる量になっていて、実際、理想気体の内部エネルギーは温度だけで表現できることが証明できます (これも後述)。

熱力学第一法則 [2] – 内部エネルギー U と微分形

 内部エネルギーが状態量であるという事実は、熱力学第一法則から分かります。準静的過程でなくてもよい、A-B に到達する任意の過程を 2 つ考えます。また、これとは別に、準静的な過程を 1 つ考えます。

009.png

図はこのようです。準静的過程である経路 3 だけは、準静的なので逆行することも可能、つまり過程 B-A をたどることが可能になっています。他は任意の過程なので不可逆であり、B から A に戻ることはできません。このうち、経路 3 の逆行過程と経路 1 または 2 のいずれかを結べば、もとの状態に戻ってくるサイクルをなします。サイクルにおいて、エネルギーの変化量は 0 になります。元の状態に戻ってきたのに、エネルギーが増減していたと仮定すると、エネルギーが何もないところから生まれたか、消滅したことになってしまいます。サイクルを運転し続けるとエネルギーが勝手に増えていくことはなく、このような機関は第一種永久機関と呼ばれ、実現不可能です。したがって、サイクルにおいては ΔU = 0 となります。たとえば、経路1 と経路 3 の逆行過程でサイクルを作ったとします。このとき、全体としての内部エネルギー変化を、サイクルを A-B 間の内部エネルギーの変化、B-A 間の内部エネルギーの変化に分割することができます。

010.png

また、B-A は準静的過程である経路 3 の逆行過程なので、マイナス符号をつければ A-B の過程に置き直すことができます。

011.png

これより、

012.png

であることが分かります。同様に、任意の過程 2 に対しても、経路 2 と経路 3 の逆行過程でサイクルを作ればただちに

013.png

であることが分かります。二つの式は右辺が同じ値であるため、最終的に

014.png

であることがわかりました。これによって、任意の 2 つの過程における内部エネルギーの変化が同じであることが分かったため、始点と終点が一致する変化は、どのような変化であっても Q-W の値が同じになるということができます。ただし、Q, W それぞれの値は異なってもよいことに注意します。あくまで等しくなるのは ΔU=Q-W だけです。
 これより、ある平衡状態からある平衡状態に移行すると、常に内部エネルギーの増減の量 (Q-W の値) が同じになることが判明しました。この事実は、熱力学第一法則では、内部エネルギーの差についてしか規定していないため、「ある点を基準点にすると、任意の点の内部エネルギーの値を定めることができる」ということを意味します。仮に、固定された基準 O (T, V, n の値は固定値) に対して、任意の点 P (T, V, n) への過程 OP を考えると、この OP 間での Q-W は経路によらないのですから、状態量である T, V, n の組み合わせによってこの変化を表現することができます。つまり、U は始点終点によってのみ定まる値なので状態量になっています。そこで、基準 O から任意の点 P (T, V, n) までの Q-W の変化量を、U(T, V, n) と定義します。

015.png

これによって、ここまでの関係図は上のようになります。通常は、暗黙のルールとして、O は記載しないでおきます (U には定数分の任意性があり、差を取ればそれが打ち消されるため、O の位置は通常問題にならない)。
 また、U は U(T, V, n) の状態量になっているので、多変数関数と見なせます。つまり、この U に対して全微分 dU が (当分は物質量は変化しない簡単な場合を考えるので、n は定数とし、T, V, P いずれかの 2 変数関数と見なします)

016.png

と定義されます。これは物理的な理由というよりは、微分積分のごく基本的なところによるものです。全微分という難しそうな名前が付いていますが、「多変数関数の微小な変化は、それぞれの変数が微小に変化したときの寄与を足し合わせたものに等しい」といっているだけです。たとえば、f(x, y) という関数があったとして、この関数が微小に df 動いたとすれば、ベクトルの分解よろしく x 方向に微小変化するときの係数 (傾き) ∂f/∂x に実際変化した量 dx を、そして y 方向にも微小変化する時の係数 ∂f/∂x に実際変化した量 dy をかけ、これら合わせたものに等しくなるというだけの話です。ここで、通常偏微分は右下に T や V などの添え字は書かないものですが、熱力学の場合固定されている変数をわざわざ添え字として書いておくことが慣習のようなので、ここでもそのような表記にならいます。
 これによって、熱力学第一法則の微分形を定義することができます。変化量 ΔU = Q-W だったことを思い出せば、この変化の幅 ΔU が微小量になる極限で、

017.png

の関係が成り立つことになります。特に、熱力学第一法則の微分形として、

018.png

が用いられます。微小熱量、仕事にプライム ( ‘ ) がついているのは、これが状態量ではない (全微分が定義できない) ことを意味しています。既に書きましたが、熱力学第一法則は Q-W の値が同じになるのであって、状態 A から Q しか吸収せず B に到達する経路や、W しか吸収せずに B に到達する経路など、Q や W そのものの値は経路に大きく依存し、同じには基本的になりません。つまり、これは始点終点によってだけでは決まらないので、状態量ではなく、関数関係が仮定できないため、全微分が定義されません。こういった量は状態量と区別する目的でプライムが付加されます。見方を変えると、熱量と仕事、状態量でないものの和をとったら状態量になった、とみることもできます。
 これが、熱力学第一法則の基本的な準備になります。次に、具体的にこの熱量や仕事といった量は、準静的過程においては求める方法が存在するという事実について調べます。それによって、最終的にサイクルの効率といったより実用的なことを調べることができるようになります。

熱力学第一法則 [3] – 仕事 W の求め方

 微小に仕事をするとき、dW は

020.png

の関係があります。これに基づいて仕事を求めていくことになります。この関係は、以下のような最も簡単な図によって導出することができます。

019.png

ピストンが dW だけ膨らみ、仕事をしたとすると、 Fdx だけ仕事をしたことになります。外界の圧力 P に抗するため、同じだけの力 F=PS が必要なので、 Fdx = PSdx ですが、 Sdx は微小体積 dV と一致します。もちろん、わずかに動けばそれに応じて圧力も変化し続けるので、P を一定で見ているのはおかしいと思うかもしれません、しかし、微少量なので結局寄与は dPdV つまり二次の微少量になるので、0 極限では無視されますから、「P の値が変化しないような微小変化での出来事」としてこのようにおいてよいのです。もし、思い出す際はこの図で十分ではありますが、これはピストンではない一般の場合でも成立します。

021.png

数学的には厳密さがないかもしれませんが、結局、dV はそれぞれの区切りを直方体と見なしてもよいような微小区間ごとに dndS の寄せ集めを行っているだけとなります。
 以上、準静的過程において、この dW を積分すれば、した仕事 W を具体的に求めることができるようになります。

熱力学第一法則 [4] – 熱量 Q の求め方

 熱量も同様に、熱力学第一法則の微分形を用いて求めることができます。熱力学第一法則の微分形を移項すると

022.png

となります。dU は状態量なので全微分

016.png

および

020.png

を代入することで

023.png

という d’Q に関する関係式が生まれます。これで、状態量ではない d’Q を状態量によって特定するための関係式が生まれたということになります。
 仕事と違って熱量は求めにくそうです。しかし、準静的過程において、熱量を d’Q を積分することによって求める必要があるのは、実質的に 2 つの場合だけです。体積を一定にした定積過程と、圧力を一定にした定圧過程の 2 つです。少し先取りすることになりますが、攻略法的な観点で行くと、熱力学という科目で取り扱う準静的過程というのは定積過程と定圧過程、断熱過程と等温過程の 4 つしかありません。このうち、断熱過程は熱量のやりとりがない過程をいうので d’Q = 0 でそもそも熱量を求める必要がありません。等温過程について、詳細は後述しますがこの場合 Q=W になるので d’Q を積分しなくても PdV の簡単な積分だけで済みます。と、いうわけで考えないといけないのは定積過程と定圧過程だけ、ということになります。
 定積過程とは、体積を一定にした場合の過程の事で、体積が変化しないため dV = 0 になります。このとき、ただちに

025.png

の関係が得られるため、

026.png

と置き換えることで、

028.png

の関係にあることがわかります。実は、理想気体の場合、内部エネルギーは T に関する一次関数になるので、T で偏微分すると定数になります (これについては後述)。つまり理想気体では Cv は定数であり、この比例定数を定積比熱といいます。比熱は、現在の温度から 1 度あげるために必要な熱量を意味することとなります。一般には定数ではないため、理想気体でなければそう簡単には積分はできません。
 定圧変化は、圧力 dP = 0 の場合の変化になります。

027.png

このあたりは機械的に処理できるところです。これが定圧比熱になります。これも後述ですが、Cp も理想気体の場合、定数です。与えた条件によって比熱の値が違っていることが分かります。熱力学で使うことになるのはこの 2 種類の比熱だけです。
 以上より、定積変化の場合熱量は d’Q = Cv dT の積分によって、定圧変化の場合熱量は d’Q = Cp dT の積分によって求まることが分かります。特に理想気体の場合、Cv, Cp は定数なので簡単に積分ができます。

準静的過程 [1] – 定積過程

 これで、d’W, d’Q を特定するための準備が整いました。実際に熱力学で考える必要がある準静的過程は 4 つあります。一つ目が体積を一定にする定積過程と呼ばれる過程です。この過程を経たとき、内部エネルギーの変化はどのように記述できるのでしょうか。

029.png

定積変化は、体積が変化しないので、一般に用いられる P-V 図、T-V 図において上のように直線で表されます。準静的過程であるので温度が上がる定積加熱と温度が下がる定積冷却というものが考えられます。
 ここで、体積は一切変化しないので、dV=0 となります。つまり、

030.png

が成り立ちます。体積が変化しないので、物体は仕事をしないのです。したがって、熱量を受け取るか、熱量を放出するかだけになります。その熱量の変化は、定積比熱 Cv を用いて、

031.png

と表現されます。理想気体の場合、Cv は定数なので積分の外に出すことができ、

032.png

であることが分かります。点 B の方が温度が高ければ、Q は正で熱量を吸収したことになり (定積加熱)、B の方が低ければ、Q は負で熱量を放出したことになります (定積冷却)。
 以上から、内部エネルギーの変化は

033.png

となります。

準静的過程 [2] – 定圧過程

 圧力を一定に保ちながら経る過程を定圧過程と言います。このとき、圧力が一定なので P = const. (一定), dP=0 が成り立っています。P-V 図においては、P 一定なので横方向の直線になります。T-V 図においては、理想気体の場合は、状態方程式より、T=(P/nR)V より、比例関係にあるので、ななめ方向の直線になります。

034.png

このとき、熱量を求めるには、既に示したように、定圧過程の場合、定圧比熱 Cp を用いることで

035.png

の積分を計算します。理想気体の場合、Cp は定数なので、求めることは容易で、

036.png

となります。
 仕事も全く同様です。

037.png

P が定数なので、積分は簡単です。
 内部エネルギーの変化は、理想気体ならば

038.png

と表現することができます。

準静的過程 [3] – 等温過程

 温度を一定に保ちながら経る過程を等温過程と言います。このとき、温度が一定なので T = const. (一定), dT=0 が成り立っています。T-V 図においては、T 一定なので横方向の直線になります。P-V 図においては、理想気体の場合は、状態方程式より、T=(nRT)/V より、半比例関係にあるので、右に進むほど落ちていく曲線になります。

039.png

このときの Q, W を特定するためには、一つ重要な実験結果を知っておく必要があります。これは何かから導出できるもの、つまり理屈というよりは単に実験結果なので、一つの事実として知っておいた方がよいものです。それがジュールの実験とよばれる実験です。これは、自由膨張という、真空に気体が膨張していく不可逆過程についての実験なのですが、

040.png

上図のように、二つの容器があり、外側は断熱されているとします (外界との熱のやりとりがない)。水とは熱平衡状態にあり、中の気体も温度 T で同じです。容器のうち、左側には気体が入っていますが、右側の容器は気体が入っておらず、真空となっています。ここで、気体が右側の真空に膨張していくことを考えるとき、真空と言うことは何も気体の分子がいないのですから、気体が膨張していっても広がり放題であり、何かの圧力に抗して広がっていく必要がないと考えられます。つまり、物体は体積が広がるにもかかわらず、仕事をしません。W=0 です。そして、広がりきった後、温度を計測した結果、温度は T のままで一切変化することがありませんでした。これが実験結果であり、言い換えれば T=const. (一定), dT=0 です。これより、温度が変化しなかったため、気体が正味吸収した熱量は 0 である、Q=0 ということができます。したがって、自由膨張においては、Q=W=0、ΔU=0 が成立します。
 ここで、ジュールの実験より、一つの事実を得ました。

温度が一定の環境下 (dT=0) で膨張する過程は、ΔU=Q-W=0

ということです。dT=0 とは、等温過程のことを言っています。簡単に言えば、自由膨張と等温過程は始点終点が同じなのです。熱力学第一法則より、Q と W の値それぞれはもちろん自由膨張とは異なりますが、U は状態量なので、ΔU = 0 であることは等温過程でもそのまま成り立ちます。したがって、等温過程においては

041.png

であることが分かります。これが正の場合、等温過程では熱量を吸収していると同時に仕事をしていることになり、負の場合、熱量を放出したかわりに同じだけ仕事をされることが分かります。理想気体では、状態方程式 PV=nRT より、

042.png

であることがわかります。

準静的過程 [4] – 断熱過程

 断熱過程は最もコツがいる変化です。その名の通り、外部との熱のやりとりを絶って経る過程のことです。d’Q=0 また Q=0 が成り立っています。

043.png

図はこのようです。P-V 図において、断熱線は等温線よりも変化が急になります (後述)。
 このことから、Q=0 で求める必要がありません。仕事は、PdV を積分すると、うまくいきません。なぜなら、他の過程と違い、P, V, T ともに動き得るからです。PdV = nRT/V dV となりますが、T は定数ではないのでそのままでは積分できません。この仕事を求める方法として、主に、後述のポアソンの法則を用いて積分可能な形に持って行く方法と、この断熱過程を準静的等温過程と準静的定積過程でつないだサイクルから求める方法があります。前者は計算が面倒なので、ここでは後者の方法を紹介します。
 問題となっている断熱過程 AB に対して、等温収縮、定積加熱を経て元の A に戻ってくるようなサイクルを考えます。

044.png

このようなサイクルです。熱力学第一法則より、サイクルの内部エネルギー変化は 0 になることから、

045.png

が成り立ちます。ここで、等温過程における内部エネルギーの変化は、ジュールの実験の結果より、

046.png

となること、および、定積過程では仕事が 0、断熱過程では熱量が 0 となることから

047.png

であり、結局、断熱過程における仕事とは、

048.png

で求まることが分かります。こうなればもう簡単です。BC は等温変化なので、B における温度と等しくなります。したがって、A と B の状態量だけでこの過程の熱量を表現することができ、理想気体の場合、

049.png

であることが分かりました。

マイヤーの関係式

 マイヤーの関係式という、理想気体における定積比熱と定圧比熱についての関係を表した式があります。

050.png

という式です。小文字の c は C=nc の関係にあります。元々の C が n モルの物体の時の比熱と定義していたので、1 モルあたりに直したときはこのような定義になるわけです。ですから上の式は ncv=ncp+nR ということもできます。これも求め方はいくつかあります。マイヤーサイクルと呼ばれる、自由膨張 (不可逆過程)、定圧過程、定積過程からなるサイクルに熱力学第一法則を適用してこの関係式を得る方法と、数学的に

027.png

から理想気体における条件を適用して計算して出す方法です。後者の方が簡単なのでここでは後者のやり方を紹介します。
 まず、理想気体の場合、内部エネルギーは T のみに依存する一変数関数になるという事実を知っておく必要があります。これはジュールの実験より導き出せる事実です。ジュールの実験を思い出すと、内部エネルギーに変化はなかったので

051.png

が成り立ちます。ここで、温度が変化しなかったことから、dT=0であり

052.png

体積には変化があるので、dV=0 ではありません。したがって、このジュールの実験結果に対して辻褄を合わせるためには、理想気体は

053.png

を満たさなければならないということになります。これは、内部エネルギーを V で偏微分したところ、0 になる、つまり内部エネルギーは体積によらないことを意味しています。したがって、理想気体では

054.png

が成立します。このことから、

055.png

であることがわかります。理想気体の場合、V=(nRT)/P ですが、一番右の偏微分は、P を定数とみなし偏微分するので、T だけが消え

056.png

の関係が分かります。したがって、モル比熱に直せば

050.png

が示せます。

ポアソンの法則