java-beginner.com ブログ

プログラミングを学習するブログ(Javaをメインに)

モンテカルロ法を抽象メソッドを使って実装した

投稿日:

最終更新日:2022年07月08日

アイキャッチ

こんにちは。今回はモンテカルロ法というアルゴリズムをJavaで実装する方法について扱います。このアルゴリズムは図形の面積の近似値を求めるアルゴリズムです。大まかに言うと、一定数のランダムな点を用意して、対象に図形に含まれる点の数で図形の近似値を求めるというものです。

この記事の前半では、標準的な記述の仕方で、モンテカルロ法による円の面積の近似値を求めるサンプルを作りました。後半では、そのサンプルプログラムの一部分を抽象メソッドを使う形に書き換えました。

モンテカルロ法の標準的な実装

今回は半径1の円の面積の近似値を求めることを目的とします。xy座標上の原点が中心の半径1の円を考えます。この円の内側でx, yが非負の数の範囲の部分を考えます。この扇形は元の円の4分の1です。この扇形の面積の近似値をモンテカルロ法で計算し、その値を4倍にすれば、元の円の面積の近似値が求まります。

モンテカルロ法で計算する部分の図
図:モンテカルロ法で計算する部分。

点(x, y)が上記の円に含まれるのは、xとyについて以下の式が成り立つ場合です。

判定式
x * x + y * y <= 1

今回のモンテカルロ法の実装では、この判定式を使います。x, y >= 0という条件の下で考えます。アルゴリズムの考え方は以下の通りです。

  1. 0 <= x <= 1かつ0 <= y <= 1を満たすランダムな点(x, y)を用意する。(点の個数をN個とする。)
  2. 各点(x, y)が判定式を満たすか調べる。(判定式を満たす点の個数をM個とする。)

このとき、M / Nが対象の図形の近似値です。

モンテカルロ法の図
図:[図形に含まれる点の個数] / [全体の点の個数]のイメージ。

プログラムの実装としては例えば以下のような手順を実装します。

  1. 以下の変数を定義する。

    修飾子と型 変数名 初期値 用途
    final int N 1000000 繰り返し回数。今回は左記を定数値とする。
    int M 0 図形に含まれるの点の個数をカウントするための変数。
    double x 0 点(x,y)のx座標。便宜上、左記を初期値とする。
    double y 0 点(x,y)のy座標。便宜上、左記を初期値とする。
  2. 以下をN回、繰り返す。

    1. x ← 「0 <= x <= 1」のランダムな値

    2. y ← 「0 <= y <= 1」のランダムな値

    3. (x, y)が判定式を満たすならば、Mを1加算する。

  3. M / Nを計算する。

以下のサンプルプログラムではcalcArea()というメソッドを作り、for文で上記の手順を実装しました。

モンテカルロ法の実装例

public class MC {

	/**
	 * モンテカルロ法で面積の近似値を求める。
	 * @return 面積の近似値。
	 */
	public double calcArea() {
		final int N = 1000000;
		int M = 0;
		double x = 0;
		double y = 0;

		for(int i = 0; i < N; i++) {
			// (1-1)
			x = Math.random();
			y = Math.random();

			// (1-2)
			if (x * x + y * y <= 1.0) {
				M ++;
			}
		}

		// (1-3)
		double area = (double)M / N;

		return area;
	}

}

(1-1)では変数x, yにランダムな値を設定しています。左辺に使っているMathクラスのrandom()メソッドは0以上1未満のランダムな小数値(double値)を返却します。変数x, yに0以上1未満の値が格納されます。この場合、点(x, y)はx=1またはy=1となる点にはなりません。この記事ではそのような点は考えないことにしました。後の計算結果を見ると、このような仮定でも近似値は得られました。

(1-2)では点(x, y)が扇形に含まるか判定しています。条件式に前述の判定式を記述しています。点(x, y)が図形に含まれる場合、この条件式の結果がtrueです。その場合に、変数Mの値を1加算しています。

(1-1)と(1-2)はfor文のブロック内に記述しています。繰り返し回数はN回です。なので、N個の点に対して、それぞれの点が扇形に含まれるかどうかを調べているということです。

(1-3)が近似値を計算している部分です。変数areaにM/Nを小数として格納しています。単純に「M / N」と記述するとint型同士の割り算(整数同士の割り算)のため、結果が整数です。「(double)M」と記述することでMをdouble型に変換しています。その結果、小数 / 整数の形になり、結果が小数(double型)です。

以下のように、上記プログラムをテストしてみました。

クラスMCのテスト

public class MCTest {

	public static void main(String[] args) {
		MC mc = new MC();

		System.out.printf("クラス%sを使った計算結果", mc.getClass().getSimpleName());
		System.out.println();

		for (int i = 1; i <= 10; i++) {
			double area = 4 * mc.calcArea();
			System.out.printf("%2d回目: %f", i, area);
			System.out.println();
		}
	}

}

結果

クラスMCを使った計算結果
 1回目: 3.141144
 2回目: 3.143888
 3回目: 3.141120
 4回目: 3.141796
 5回目: 3.141040
 6回目: 3.140260
 7回目: 3.140000
 8回目: 3.139688
 9回目: 3.142216
10回目: 3.143616

上記はテスト用に作ったクラスです。main()メソッドでcalcArea()メソッドの結果を4倍した値を出力しています。この値が円の面積の近似値です。半径1なので円の面積は円周率です。出力結果はその近似値です。for文を使って10回計算していますが、計算結果は1回ごと異なります。

なお、「getClass().getSimpleName()」ではクラス名を取得しています。

抽象メソッドを使う

今回の記事では、上記サンプルのif文の箇所の条件式を抽象メソッドに書き換えることを考えます。

方法1:抽象クラスにする

Javaのクラスを自作したとき、メソッドの宣言を記述した場合は、その処理内容も記述する必要があります。あるメソッドの処理内容の記述がない状態にしたい場合、クラス宣言にabstract修飾子を追加します。例えば、「public abstract class 〇〇〇」という書き方です。このようなクラスは抽象クラスと呼ばれます。

また、処理内容がないメソッドにもabstract修飾子を追加します。例えば、「public abstract boolean 〇〇〇」という書き方です。このようなメソッドは抽象メソッドと呼ばれます。

以下は上記クラスMCを抽象クラスに書き換えた例と、それを継承したクラスの例です。

抽象クラス

public abstract class MCAbs {

	/**
	 * モンテカルロ法で面積の近似値を求める。
	 * @return 面積の近似値。
	 */
	public double calcArea() {
		final int N = 1000000;
		int M = 0;
		double x = 0;
		double y = 0;

		for(int i = 0; i < N; i++) {
			// (2-1)
			x = Math.random();
			y = Math.random();

			// (2-2)
			if (contains(x, y)) {
				M ++;
			}
		}

		// (2-3)
		double area = (double)M / N;

		return area;
	}

	// (2-4)
	/**
	 * 点(x, y)を含むか判定するメソッド。
	 */
	public abstract boolean contains(double x, double y);

}

MCAbsを継承したクラス

public class MCEx extends MCAbs {

	@Override
	public boolean contains(double x, double y) {
		return x * x + y * y <= 1.0;
	}

}
クラス図
クラス図:MCExはMCAbsを継承しています。

MCAbsという名前で抽象クラスを作りました。

クラスMCに対して、(2-2)の部分を書き換えました。if文の条件式の箇所にcontains()メソッドを使っています。このメソッドは(2-4)で抽象メソッドとして宣言しています。大まかにいうと、以下のような内容の宣言です。

  • メソッドの引数はdouble xとdouble y。
  • メソッドが返却する値はboolean型の値。
  • 処理内容の実装はしていない。

abstract修飾子を付与しているため、処理内容を記述しない形にできます。今回の記述では、メソッドの返却する値がboolean型の値であるため、if文で条件式の代わりにこのメソッドを使用できます。

上記の抽象クラスを継承したクラスをMCExという名前で作りました。このクラスではcontains()メソッドのオーバーライドをしています。内容は最初に作ったクラスMCのif文の条件式で記述している内容です。

以下はMCExの動作確認です。

クラスMCExのテスト

public class MCAbsTest {

	public static void main(String[] args) {
		MCAbs mc = new MCEx();

		System.out.printf("クラス%sを使った計算結果", mc.getClass().getSimpleName());
		System.out.println();

		for (int i = 1; i <= 10; i++) {
			double area = 4 * mc.calcArea();
			System.out.printf("%2d回目: %f", i, area);
			System.out.println();
		}
	}

}

結果

クラスMCExを使った計算結果
 1回目: 3.141312
 2回目: 3.142748
 3回目: 3.140432
 4回目: 3.141132
 5回目: 3.140252
 6回目: 3.138576
 7回目: 3.140180
 8回目: 3.143320
 9回目: 3.141708
10回目: 3.139560

インスタンスを生成している個所は「MCAbs mc = new MCEx();」です。MCAbs型変数にそれを継承したクラスのインスタンスを格納しています。クラスMCAbsは抽象クラスなので、このクラスのインスタンスは生成できません。実際に「new MCAbs();」と記述すると「型 MCAbs のインスタンスを生成できません」というコンパイルエラーが発生します。

MCAbs型変数にクラスMCExのインスタンスを格納しているため、「mc.calcArea()」の箇所ではクラスMCExのcalcArea()メソッドが呼び出されます。

このように抽象クラスを使うことで、継承後のクラスの方に具体的な処理の記述する方式にできます。

方法2:インターフェースをフィールドに持たせる

抽象クラスを使う方法の他に、インターフェースを使う方法があります。

インターフェース

public interface MyShape {
	/**
	 * 点(x, y)を含むか判定するメソッド。
	 */
	public boolean contains(double x, double y);
}

上記がインターフェースの例です。contains()メソッドを抽象メソッドに持ちます。MCAbsの場合と同じように、このメソッドの引数は点(x, y)の各座標の値、返却する値はboolean型という宣言です。

このインターフェースを使って、最初に作ったクラスMCを以下のように書き換えました。

インターフェースを利用するクラス

public class MCWithInterface {

	// (3-1)
	MyShape myShape;

	// (3-2)
	public MCWithInterface(MyShape myShape) {
		this.myShape = myShape;
	}

	/**
	 * モンテカルロ法で面積の近似値を求める。
	 * @return 面積の近似値。
	 */
	public double calcArea() {
		final int N = 1000000;
		int M = 0;
		double x = 0;
		double y = 0;

		for(int i = 0; i < N; i++) {
			x = Math.random();
			y = Math.random();

			// (3-3)
			if (myShape.contains(x, y)) {
				M ++;
			}
		}

		double area = (double)M / N;

		return area;
	}

}
クラス図
クラス図:MCWithInterfaceはMyShapeをフィールドに持っています。

(3-1)でフィールドにMyShape型の変数を定義しています。(3-2)はコンストラクタです。引数をフィールドに格納しています。

(3-3)のif文の条件式ではMyShapeのcontains()メソッドを呼び出しています。

上記のクラスMCWithInterfaceではインターフェースの具体的な実装の記述はありません。今回はこのクラスのインスタンスを生成する箇所で、インターフェースの実装を記述する方法を使うことにしました。

以下は、上記クラスの動作確認用のクラスです。

クラスMCWithInterfaceのテスト

public class MCWithInterfaceTest {

	public static void main(String[] args) {
		MCWithInterface mc= new MCWithInterface(
				(x, y) -> x * x + y * y <= 1.0);

		System.out.printf("クラス%sを使った計算結果", mc.getClass().getSimpleName());
		System.out.println();

		for (int i = 1; i <= 10; i++) {
			double area = 4 * mc.calcArea();
			System.out.printf("%2d回目: %f", i, area);
			System.out.println();
		}
	}

}

結果

クラスMCWithInterfaceを使った計算結果
 1回目: 3.142592
 2回目: 3.140100
 3回目: 3.142364
 4回目: 3.139832
 5回目: 3.141288
 6回目: 3.141856
 7回目: 3.139460
 8回目: 3.142168
 9回目: 3.140436
10回目: 3.143836

最初にnewでMCWithInterfaceのインスタンスを生成しています。引数にはMyShapeインターフェースの実装クラスのインスタンスを指定する必要があります。今回の場合はラムダ式を使って、実装クラスの宣言を記述しました。

MyShapeインターフェースには抽象メソッドがひとつだけ定義してあります。このようなインターフェースは関数型インターフェースと呼ばれます。関数型インターフェースの実装クラスはラムダ式で記述することができます。「new クラス名([ラムダ式]);」という書き方をしています。

[ラムダ式]の箇所は[引数の組] -> [return文]という書き方です。[return文]の個所は省略形として「return」という記述は省略できます。今回は「return (条件式);」を省略形で記述しています。条件式には、この記事の最初に登場した、点が円に含まれるかの判定式を記述しています。

このように、インターフェースを使うことで、処理内容をクラス外から引数で渡す方式にできます。

以上、参考になれば幸いです。