バラバラと、向き合う。#2
私はよく、バラバラについて、考えている。
(↑よく引用される記事だこと)
マーダーミステリーのことを書いたりしている人が、バラバラについて考えているというと、非常にアブナい響きがあるわけなのだけれども、今日はそっちのバラバラではなくて、乱数の方のバラバラの話をしたい。
ランダム関数
たいてい、どんなプログラミング言語にも、「ランダム(乱数生成)関数」というものが用意されています。
import java.util.Random;
public class Main {
public static void main(String[] args) {
// Randomクラスのインスタンスを作成
Random random = new Random();
// 0以上1未満のランダムな浮動小数点数を生成
double randomNumber = random.nextDouble();
System.out.println("Random Number without Seed: " + randomNumber);
}
}
↑は、ChatGPTに生成してもらった、JavaでのRandom関数の使用例です。Javaの場合は、Randomというクラスが事前に用意されているので、importで呼び出して、インスタンスを作成して使います。他の言語でも、rand()とか、random()とかrandomize()とかちょっとした違いはあると思いますが、同じような方法で呼び出せます。
で、大抵は、0から1未満(0.999999…)の無限小数(とはいいながら有限なんだけど限りなく長いから無限ってことにしといてね!)として値が得られるので、「サイコロを振って出た目」みたいな使い方をしたい場合は、特定の値を掛けてから、小数点以下を切り上げる、みたいな計算をして、欲しい整数値を取り出します。
C#の思い出
私は、新しいプログラミング言語に出会うと、とりあえずヤッツィーを作るタイプの人間なので、C#という玩具に出会った時真っ先に、1人で遊べる「GREED$」を実装しました。(GREED$は世界で1番簡単で面白いゲームなので、遊んだことのない人は是非遊んでくださいね。)
で、サイコロを振る部分は、当然、上記のような乱数生成関数を使うわけなのですが、これがちょっと曲者で、下記のような事象が起きました。
・起動するたびに、同じサイコロの出目から始まる。(例:いつでも、1-1-4-4-4-6から始まる)
また、実装を繰り返すうちに、
・サイコロの出目が、別の乱数生成器から目を生成しているのに、おなじになる。(4-4-4-4-4-4の次が2-2-2-2-2-2みたいな)
まぁ、実際はGREED$を作っているので、G-G-E-E-E-$やE-E-E-E-E-Eと表示されているのですが…。
再現可能な乱数
基本的に、コンピュータが生成する乱数は、再現が可能なように、作られています。ですので、乱数とは言いながらも、コンピュータにとっては、順番の決まった、複雑な値の数の列、ということになります。
で、ここで出てくる概念に「種(Seed)」というものがありまして。お約束として、同じ種からは、同じ数列を発生させようね、ということになっています。
import java.util.Random;
public class Main {
public static void main(String[] args) {
// シード値を指定してRandomクラスのインスタンスを作成
long seed = 42; // 任意のシード値
Random random = new Random(seed);
// 0以上1未満のランダムな浮動小数点数を生成
double randomNumber = random.nextDouble();
System.out.println("Random Number with Seed: " + randomNumber);
}
}
この例では、「long seed = 42」としているので、「42」とラベルの付いた種から乱数を取り出すぞ、という宣言をしていることになります。そうすると、例えばですが、毎回、最初が0.2464、次に0.8157、3番目が0.1149…のように、一見ランダムだけれど、毎回決まった順番で小数値が得られる、という結果になります。
私がハマった例の1番目は、毎回、起動するたびに期せずして同じSeed値を指定してしまっていたので、同じ順番でサイコロの目が表示されてしまったのです。そして2番目の例の原因は、言語仕様で、Seed値を指定しないと、Seedのところを現在時刻(ミリ秒)にするね、というものがあったのですが、1ミリ秒の間にSeedを6個生成してしまっていて、別の乱数生成器を作っているつもりが、同じSeed値の乱数生成器を6個作ってしまっていたため、でした。
単純に乱数を得たい場合の解決策というのは、いろいろあるのですが、私が好きなのは、Random関数を2つ呼んで、片方のRandomから値を1つ取り出して、もう一方のRandom関数のSeedとして使う、というやり方です。
import java.util.Random;
public class Main {
public static void main(String[] args) {
// 最初のRandomインスタンスを作成
Random firstRandom = new Random();
// 最初のRandomから1つの乱数を生成
int seedValue = firstRandom.nextInt();
// 2つ目のRandomインスタンスを作成し、先ほど生成した乱数をSeedとして設定
Random secondRandom = new Random(seedValue);
// それぞれのRandomから乱数を生成して表示
System.out.println("Random Number from the first Random: " + firstRandom.nextDouble());
System.out.println("Random Number from the second Random with the seed from the first Random: " + secondRandom.nextDouble());
}
}
なぜ突然こんな話を始めたのか
ここまでの話で終わると、「苦労したんだね」という話で終わるわけなのですが、実はこの話、これまでの2つのジャンルの記事とリンクしています。
1つ目は、昨日まとめを書いた、「最適化問題」のジャンルで、「探索の起点はランダムで決めたらいい」というようなことを書いているのですが、この時、Seed値は記録しておけるようにしようね、ということを言いたかったのです。こういう、実験めいたことをしている時に、ものすごく良い結果が出て、「もう1回、同じ実験をしてみたい」という場面が必ずやってきます。ここでSeed値が指定されていないと、もう2度と同じ結果には出会えないのですが、Seed値を指定できると、(反復コードの書き方によっては)同じ結果が得られます。
2つ目は、最近ドハマりしている、「画像生成系AI」の話です。あのAIは、ざっくり言うと、学習の過程で、完璧な絵を何度も何度も叩き壊し、「ああ、このパーツを叩き壊すとこんな風に壊れていくのか…」という体験を無数に繰り返していくことで、「壊れた方の欠片をみて、元のパーツがこんな風になっているんじゃないか」という能力を得た人工知能です。
この人工知能に与えるインプットデータは、ランダムに生成された、ノイズ画像になります。(今の人はテレビからノイズが出ないから、何と言って説明していいのかわかりませんね)
この、「元になるノイズ画像」が同じであれば、生成したAI画像は、同じものになります。
ここで大事になってくるのが、元のノイズ画像を作る時にも、何かしらのランダム関数が使われているので、Seed値を同じにする→元になるノイズ画像が同じになる→同じ画像が生成される!ということです。
まとめ
というわけで、今回は、エンジニアであれば一度は「ランダムシード」と「ガンダムシード」って1文字違いだよね、って思うよね、という話をしてきました。(していない)これを機に、私と同じように、バラバラについて考える人が増えると面白いな、と思っています!ほなね!
例のAI画像のコーナー
今日は、サイコロを持った人を生成したいと思っています。
というわけで最終的に落ち着いたプロンプトは、「(8k, RAW photo, best quality, masterpiece, neonpunk:1.2), Slender dealer girl, Casino, (Dice:1.5), looking at viewer」です。ネオンパンクってこういう世界観なんですね。
どうも、ルービックキューブとか、サイコロとか、立方体のものをきちんと生成するのは苦手みたいですね。
いただいたサポートは、きっと、ドイツアマゾンからの送料に変わると思います。 温かいご支援、お待ちしております。