通常のグリッド (grid1) と予測グリッド (grid2) を比較し、セルの値が異なる場合に赤色や青色で描画するようにしています。具体的には、次のルールで色を決定します:
両方のグリッドが1 → 黒。
両方のグリッドが0 → 白。
grid1 が1で grid2 が0 → 赤。
grid1 が0で grid2 が1 → 青。
これにより、通常のグリッドと予測グリッドの間の差異を視覚的に確認できるようになっています。
ベクトル型メモリーアクセスの概念
通常のメモリーアドレスは、リニアなアドレス空間(整数のアドレス)に基づいています。これは整数という1次元の特殊なベクトルとも考えられます。しかし、ベクトル型メモリーでは、アドレスはベクトルで表され、例えばデータの特徴量や属性に基づいたベクトルが使用されると考えることができます。アクセスする際には、与えられたベクトルに対して コサイン類似度 などの手法を用いて、最も類似するメモリーアドレス(ベクトル)を見つけ、そこからデータを取得します。
論文風解説
はじめに
ライフゲームは、単純なルールに基づいて進化するセル・オートマトンの一種であり、複雑なパターンや動的挙動を生成することで広く知られている。本研究では、ライフゲームの進化過程を新たなアプローチで制御するために、過去のパターンデータを利用してコサイン類似度に基づく状態予測を行う手法を提案する。具体的には、過去のライフゲームパターンをベクトル化し、現在のパターンとの類似度を行列内積によって計算する。そして、ソフトマックス関数を用いてこの類似度を確率分布に変換し、次の状態を確率的に選択する。これは、一層のニューラルネットワークに類似したモデルであり、効率的なパターン予測を実現する。(この手法は計算コストのかかるトレーニングを必要としない。過去の100パターン行列を直接最適化された重み行列として扱う。)
コサイン類似度によるパターン予測
コサイン類似度は、二つのベクトル間の角度に基づいて類似度を計算する手法であり、値域が0から1の間に収まるため、パターンの比較に適している。現在のライフゲームの状態を一次元ベクトル化し、過去の100パターンを行列として保持する。各パターンに対するコサイン類似度がコード内で計算される。
この類似度を基に、過去のどのパターンが現在の状態に最も近いかを判定し、次の状態予測に役立てる。
ソフトマックス関数による確率分布の生成
類似度計算後、得られた値を直接使用して次の状態を選択するのではなく、ソフトマックス関数を適用して確率分布に変換する。この操作により、最も類似しているパターンが高い確率で選ばれるが、他のパターンも一定の確率で選択される余地が残る。
この確率分布に基づいて、ランダムに次のパターンが選ばれ、次のライフゲームの状態が描画される。
コードの概要
本研究で実装したコードでは、まず現在のライフゲームの状態をベクトル化し、過去100パターンに対して内積を計算することにより、各パターンとのコサイン類似度を算出する。その後、ソフトマックス関数を用いて類似度を確率分布に変換し、この確率に基づいて次のライフゲームの状態を決定する。この一連のプロセスは、シンプルな一層のニューラルネットワークに似た処理を実行し、過去のデータを利用した状態予測を実現している。
結論
コサイン類似度に基づくライフゲームの状態予測は、従来のルールベースの進化とは異なるアプローチであり、過去のパターン情報を活用することでより多様な進化パターンを生成できる可能性を示した。また、ソフトマックス関数による確率的な状態選択は、予測の柔軟性を高める効果を持つ。今後の研究では、この手法を用いてライフゲームのさらなる進化挙動を解析することが期待される。
参考文献
通常のグリッド (grid1) と予測グリッド (grid2) を比較し、セルの値が異なる場合に赤色や青色で描画するようにしています。具体的には、次のルールで色を決定します:
両方のグリッドが1 → 黒。
両方のグリッドが0 → 白。
grid1 が1で grid2 が0 → 赤。
grid1 が0で grid2 が1 → 青。
これにより、通常のグリッドと予測グリッドの間の差異を視覚的に確認できるようになっています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ライフゲーム - 画面いっぱいに描画</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<style>
body {
margin: 0; /* ページの余白を無くす */
}
canvas {
display: block; /* キャンバスを画面いっぱいに表示 */
}
</style>
</head>
<body>
<script>
// グリッドと予測グリッド
let grid;
let predictedGrid;
let cols, rows;
let resolution = 20; // 1セルあたりのサイズ(ピクセル)
let history = []; // 過去のゲーム状態を保存する配列
let historyLimit = 100; // 保存する過去状態の最大数
// 初期設定
function setup() {
// キャンバスを画面いっぱいに設定
createCanvas(windowWidth, windowHeight);
cols = floor(width / resolution); // 列数を計算
rows = floor(height / resolution); // 行数を計算
// グリッドと予測グリッドを初期化
grid = make2DArray(cols, rows);
predictedGrid = make2DArray(cols, rows);
// 各グリッドにランダムな値(0または1)を設定
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j] = floor(random(2));
predictedGrid[i][j] = grid[i][j]; // 初期状態は同じ値
}
}
}
// 画面サイズが変更されたときにキャンバスをリサイズ
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
cols = floor(width / resolution);
rows = floor(height / resolution);
// グリッドのサイズもリサイズ後に合わせて初期化する
grid = make2DArray(cols, rows);
predictedGrid = make2DArray(cols, rows);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j] = floor(random(2));
predictedGrid[i][j] = grid[i][j];
}
}
}
function draw() {
background(255); // 背景を白に設定
// 通常のグリッドと予測グリッドの違いを色で描画
drawDifferenceGrid(grid, predictedGrid, 0, 0, cols, rows);
// 次のステートを計算するための空のグリッド
let next = make2DArray(cols, rows);
// 現在のグリッドを履歴に追加
if (history.length < historyLimit) {
history.push(grid.map(arr => arr.slice())); // 配列のディープコピー
} else {
history.shift(); // 古い履歴を削除
history.push(grid.map(arr => arr.slice()));
}
// 現在のグリッドをもとに、次の状態を予測
let nextPredicted = predictNextState(grid);
// 通常のライフゲームのルールに従って次の状態を計算
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
next[i][j] = calculateNextState(grid, i, j);
}
}
// グリッドを更新
grid = next;
predictedGrid = nextPredicted;
}
// 2つのグリッドの違いを色で描画する関数
function drawDifferenceGrid(grid1, grid2, xOffset, yOffset, cols, rows) {
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
let x = i * resolution + xOffset; // x座標
let y = j * resolution + yOffset; // y座標
// 両方のグリッドが1の場合 → 黒色
if (grid1[i][j] == 1 && grid2[i][j] == 1) {
fill(0); // 黒
}
// 両方のグリッドが0の場合 → 白色
else if (grid1[i][j] == 0 && grid2[i][j] == 0) {
fill(255); // 白
}
// 通常のグリッドが1で、予測グリッドが0の場合 → 赤色
else if (grid1[i][j] == 1 && grid2[i][j] == 0) {
fill(255, 0, 0); // 赤
}
// 通常のグリッドが0で、予測グリッドが1の場合 → 青色
else if (grid1[i][j] == 0 && grid2[i][j] == 1) {
fill(0, 0, 255); // 青
}
stroke(0); // 枠線を黒に設定
rect(x, y, resolution, resolution); // 各セルを描画
}
}
}
// セルの次の状態を計算する関数(通常のライフゲームのルールに基づく)
function calculateNextState(grid, x, y) {
let state = grid[x][y]; // 現在のセルの状態
let neighbors = countNeighbors(grid, x, y); // 隣接セルの数をカウント
// ライフゲームのルールに従って次の状態を決定
if (state == 0 && neighbors == 3) {
return 1; // 誕生(死んだセルが隣接セル3つで生まれる)
} else if (state == 1 && (neighbors < 2 || neighbors > 3)) {
return 0; // 過疎または過密(セルが死滅する)
} else {
return state; // 状態を維持
}
}
// グリッドの隣接セルの生存数をカウントする関数
function countNeighbors(grid, x, y) {
let sum = 0; // 隣接セルの生存数の合計
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
let col = (x + i + cols) % cols; // 列の範囲をループ
let row = (y + j + rows) % rows; // 行の範囲をループ
sum += grid[col][row]; // 隣接セルの値を合計
}
}
sum -= grid[x][y]; // 自分自身の値を引く
return sum; // 隣接セルの生存数を返す
}
// 現在の状態を基に、過去の履歴から次の状態を予測する関数
function predictNextState(currentGrid) {
let currentFlat = currentGrid.flat(); // 現在のグリッドを1次元配列に変換
// 履歴が空の場合、現在のグリッドをそのまま返す
if (history.length === 0) return currentGrid.map(arr => arr.slice());
// 履歴を1次元配列として扱い、行列化
let historyMatrix = [];
for (let h = 0; h < history.length; h++) {
historyMatrix.push(history[h].flat());
}
// 類似度を計算
let similarities = matrixVectorDotProduct(historyMatrix, currentFlat);
// 類似度をソフトマックス関数で確率分布に変換
let probabilities = softmax(similarities);
// 確率分布に基づいて、次の履歴を選択
let selectedPatternIndex = weightedRandom(probabilities);
// 選択した履歴の次の状態を返す
if (selectedPatternIndex < history.length - 1) {
return history[selectedPatternIndex + 1];
} else {
return currentGrid.map(arr => arr.slice()); // 最後の場合は現在の状態を維持
}
}
// ベクトルと行列の内積を計算する関数
function matrixVectorDotProduct(matrix, vector) {
let result = [];
for (let i = 0; i < matrix.length; i++) {
let dotProduct = 0;
for (let j = 0; j < vector.length; j++) {
dotProduct += matrix[i][j] * vector[j];
}
result.push(dotProduct);
}
return result;
}
// ソフトマックス関数で確率を計算
function softmax(values) {
let exps = values.map(v => Math.exp(v)); // 各値の指数を計算
let sumExps = exps.reduce((a, b) => a + b, 0); // 指数の和を計算
return exps.map(e => e / sumExps); // 確率に変換
}
// 確率に基づいてランダムに選択する関数
function weightedRandom(probabilities) {
let r = Math.random(); // 0から1のランダムな数
let cumulative = 0;
for (let i = 0; i < probabilities.length; i++) {
cumulative += probabilities[i]; // 累積確率を計算
if (r < cumulative) {
return i; // ランダムな確率で選択
}
}
return probabilities.length - 1; // 最後の要素をデフォルトに
}
// 2次元配列を生成する関数
function make2DArray(cols, rows) {
let arr = new Array(cols); // 列数だけの配列を作成
for (let i = 0; i < arr.length; i++) {
arr[i] = new Array(rows); // 各列に行数だけの配列を追加
}
return arr;
}
</script>
</body>
</html>
説明:
ベクトルと行列の内積計算:
matrixVectorDotProduct関数で、現在のゲーム状態(ベクトル)と過去のパターンデータ(行列)との内積を計算しています。この計算は、一層のニューラルネットワークのように機能します。
ソフトマックス関数:
類似度をソフトマックス関数に入力して確率分布を生成します。この結果は、次のステートを選択するための確率を提供します。
確率に基づく次のステート選択:
weightedRandom関数を使って、ソフトマックス関数で得られた確率に基づき、次のパターンを選択しています。