目次
進化的アルゴリズム簡易版を試してみる
AIによるアート制作が注目を集める中、「進化的アルゴリズム」を使った創作の可能性にも注目が集まっています。
今回はJavaScriptとp5.jsを使って、進化的アート制作の初歩を体験してみた事例をご紹介します。
特別なGPUや大規模なAIモデルを使わなくても、ブラウザさえあれば簡単に始められる進化的アートです。
今回使用したのは、HTML + JavaScript + p5.jsによる進化的コードです。
描画対象は「抽象的な線のパターン」で、これらの形状は「遺伝子」として扱われ、自動的に進化(改善)されていきます。
進化的アートの基本構造
このシステムでは以下の要素が組み合わさって動作します。
Genotype(遺伝子):線の本数、色相、線の太さ、ノイズ、複雑さ、回転速度といったビジュアルのパラメータ。
Phenotype(表現型):p5.jsを用いて実際にキャンバス上に描画されるアート。
進化の仕組み:選択、交叉(クロスオーバー)、突然変異(ミューテーション)を自動で繰り返し、次世代のパターンを生成。
進化の1サイクルでは、6つのパターンが画面上に表示され、毎世代ランダムに2体の親が選ばれて子を生み出します。
自動モードでは「30世代ずつ進化」ボタンを押すことで進化を継続でき、結果としてより興味深く洗練されたパターンが現れていきます。
コードの特徴
p5.jsというJavaScriptライブラリを使うことで、グラフィック描画の制御が簡単にできます。
※ pythonプラスHugging faceのコンビも作ったのですが、Google colabからHugging faceへのアクセスがうまくいかず、一旦保留としました。
(以前よりHugging faceへのアクセスが失敗することが多いです。Huggingさんお手柔らかに)
自動進化モード:ボタンをクリックするだけで、世代をまたいで自動でパターンが変化。
ミューテーション(突然変異):色味や形の微妙な変化が自然発生。
インタラクティブ選択:マウスでパターンをクリックすると、その個体の情報と小型プレビューが表示される仕組み。
ブラウザ上で動作するため、誰でもすぐに試せるのも大きな利点です。
表示された図形はどれも抽象的なものが多く、生成過程そのものが一種の進化体験となります。
⚫︎コードを下記に記します。
ご自由に改変して使ってください。
ちなみに初期のコードですので、改変の余地ありです。
・最初にいびつな形の図形が世代を重ねていくと、正円に変化していきます。
このコードが進化的アルゴリズムを使っているかと言えば、そうでないという方もいるでしょう。
ただ「世代を重ねていくと進化していく」というのはビジュアル的に実感できるかと。
千里の道も〇〇から。まずはここからです。
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script> <style> body { font-family: 'Arial', sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; display: flex; flex-direction: column; align-items: center; }<br /> #controls { margin-bottom: 20px; padding: 15px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); text-align: center; width: 90%; max-width: 750px; }<br /> #controls button, #controls select { padding: 10px 15px; margin: 5px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; }<br /> #controls select { background-color: #555; color: white; }<br /> #controls button:hover:not(:disabled) { background-color: #0056b3; }<br /> #controls button:disabled { background-color: #ccc; cursor: not-allowed; }<br /> #generation-counter { font-weight: bold; margin-top: 10px; font-size: 1.1em; }<br /> #auto-evolution-status { min-height: 1.2em; margin-top: 5px; color: #555; }<br /> #genotype-display { display: flex; flex-wrap: wrap; justify-content: space-around; width: 100%; margin-top: 15px; margin-bottom: 10px; padding: 0px; font-size: 0.85em; text-align: left; box-sizing: border-box; }<br /> #genotype-display > div { width: calc(50% - 20px); min-width: 280px; margin: 10px; padding: 10px; border: 1px solid #ccc; background-color: #fdfdfd; min-height: 150px; box-sizing: border-box; border-radius: 4px; }<br /> #genotype-display h4 { margin-top: 0; margin-bottom: 8px; font-size: 1.1em; color: #333; border-bottom: 1px solid #eee; padding-bottom: 5px; }<br /> #genotype-display p { margin: 4px 0; line-height: 1.4; }<br /> #current-best-preview img { border:1px solid #999; display:none; margin-top: 5px;}<br /> #canvas-container { border: 1px solid #ddd; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-top: 10px; }<br /> </style> <div id="controls"> <div><label for="target-shape-selector">Target Shape: </label> <select id="target-shape-selector"> <option selected="selected" value="circle">Circle</option> <option value="square">Square (Approx.)</option> <option value="triangle">Triangle (Approx.)</option> <option value="random">Random (No Target)</option> </select></div> <button id="start-auto-evolution-button">Start Auto Evolution (30 Gens)</button> <button id="continue-evolution-button" style="display: none;">Continue Evolution (+30 Gens)</button> <button id="reset-button">Reset Population</button> <p id="generation-counter">Generation: 0</p> <div id="genotype-display"> <div id="current-best-info"> <h4>Best of Generation (Gen 0)</h4> Evolution not started. </div> <div id="current-best-preview"> <h4>Preview</h4> <img id="current-best-img" alt="Current Best Preview" width="120" height="120" /> </div> </div> </div> <div id="canvas-container"></div> <script> // --- Genetic Algorithm Parameters --- const POP_SIZE_DISPLAY = 6; // For main grid display const INTERNAL_POP_SIZE = 20; const MUTATION_RATE = 0.1; // Adjusted const MUTATION_STRENGTH = 0.1; // Adjusted const TOURNAMENT_SIZE = 2; // Adjusted // --- Global Variables --- let population = []; let generationCount = 0; let autoEvolving = false; let currentAutoEvolutionStep = 0; let currentTargetShape = 'circle'; let canvasWidth = 600; let canvasHeight = 400; let cols = 3; let rows = 2; let cellWidth, cellHeight; let currentBestInfoDiv, currentBestImg, generationCounterP, autoEvolutionStatusP; let startAutoEvolutionButton, continueEvolutionButton, targetShapeSelector; let pgBest; // --- Genotype Class --- class Genotype { constructor(genes) { if (genes) { this.numLines = genes.numLines; this.baseHue = genes.baseHue; this.strokeW = genes.strokeW; this.noiseScale = genes.noiseScale; this.shapeComplexity = genes.shapeComplexity; this.rotationSpeed = genes.rotationSpeed; } else { this.numLines = floor(random(20, 150)); this.baseHue = random(0, 360); this.strokeW = random(1, 3); this.noiseScale = random(0.01, 0.4); // Initial noise can be higher this.shapeComplexity = random(2.5, 12.5); this.rotationSpeed = random(-0.005, 0.005); } } clone() { return new Genotype({ ...this }); } } // --- p5.js Setup Function --- function setup() { let canvas = createCanvas(canvasWidth, canvasHeight); canvas.parent('canvas-container'); colorMode(HSB, 360, 100, 100, 100); angleMode(RADIANS); cellWidth = canvasWidth / cols; cellHeight = canvasHeight / rows; pgBest = createGraphics(120, 120); pgBest.colorMode(HSB, 360, 100, 100, 100); pgBest.angleMode(RADIANS); currentBestInfoDiv = document.getElementById('current-best-info'); currentBestImg = document.getElementById('current-best-img'); generationCounterP = document.getElementById('generation-counter'); autoEvolutionStatusP = document.getElementById('auto-evolution-status'); startAutoEvolutionButton = document.getElementById('start-auto-evolution-button'); continueEvolutionButton = document.getElementById('continue-evolution-button'); targetShapeSelector = document.getElementById('target-shape-selector'); currentTargetShape = targetShapeSelector.value; initializePopulation(); startAutoEvolutionButton.addEventListener('click', () => startEvolutionCycle(30)); continueEvolutionButton.addEventListener('click', () => startEvolutionCycle(30)); document.getElementById('reset-button').addEventListener('click', resetEvolution); targetShapeSelector.addEventListener('change', (event) => { currentTargetShape = event.target.value; console.log("Target shape changed to:", currentTargetShape); resetEvolution(); }); noLoop(); } function draw() { /* Paused by noLoop() */ } // --- Initialization --- function initializePopulation() { population = []; for (let i = 0; i < INTERNAL_POP_SIZE; i++) { population.push(new Genotype()); } generationCount = 0; updateGenerationCounter(); if (population.length > 0) { let initialFitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape)); let bestInitialFitness = -Infinity; // Start with -Infinity let bestInitialIndividual = population[0]; for(let i=0; i<population.length; i++){ if(initialFitnessScores[i] > bestInitialFitness){ bestInitialFitness = initialFitnessScores[i]; bestInitialIndividual = population[i]; } } updateBestIndividualDisplay(bestInitialIndividual, generationCount, false, bestInitialFitness); } else { updateBestIndividualDisplay(null, generationCount); } displayPopulationSubset(); autoEvolving = false; startAutoEvolutionButton.disabled = false; targetShapeSelector.disabled = false; continueEvolutionButton.style.display = 'none'; autoEvolutionStatusP.innerText = ""; currentAutoEvolutionStep = 0; } function resetEvolution() { autoEvolving = false; initializePopulation(); } // --- Drawing Functions --- function displayPopulationSubset() { background(250); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { let index = c + r * cols; if (index < POP_SIZE_DISPLAY && index < population.length) { // Display from actual population let xPos = c * cellWidth; let yPos = r * cellHeight; push(); translate(xPos, yPos); stroke(200); strokeWeight(1); noFill(); rect(0, 0, cellWidth, cellHeight); drawPhenotype(population[index], cellWidth / 2, cellHeight / 2, cellWidth, cellHeight); pop(); } else { push(); translate(c * cellWidth, r * cellHeight); stroke(220); strokeWeight(1); noFill(); rect(0, 0, cellWidth, cellHeight); pop(); } } } } function drawPhenotype(geno, centerX, centerY, w, h, pg = null) { const target = pg || window; target.push(); if (pg) { pg.background(240); pg.strokeWeight(1); pg.stroke(200); pg.noFill(); pg.rect(0,0,pg.width-1, pg.height-1); } target.translate(centerX, centerY); target.stroke(geno.baseHue, 80, 90, 80); target.strokeWeight(geno.strokeW); target.noFill(); let time = geno.baseHue / 360 + generationCount * 0.005; target.rotate(time * geno.rotationSpeed); target.beginShape(); for (let i = 0; i < floor(geno.numLines); i++) { // Ensure numLines is int let angle = map(i, 0, floor(geno.numLines), 0, TWO_PI); let baseRadius = min(w, h) * 0.35; let xOff = map(cos(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5); let yOff = map(sin(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5); let noiseVal = noise(xOff + geno.baseHue * 0.01, yOff + geno.strokeW * 0.1, time * 0.2 + geno.strokeW * 0.05); let r_dev = map(noiseVal, 0, 1, -min(w,h) * 0.1 * geno.noiseScale * 10, min(w,h) * 0.1 * geno.noiseScale * 10); let r = baseRadius + r_dev; let x_coord = r * cos(angle); let y_coord = r * sin(angle); target.vertex(x_coord, y_coord); } target.endShape(CLOSE); target.pop(); } // --- Fitness Calculation --- function getPhenotypeVertices(geno, simW = 100, simH = 100) { let vertices = []; let time = geno.baseHue / 360 + generationCount * 0.005; let rotation = time * geno.rotationSpeed; for (let i = 0; i < floor(geno.numLines); i++) { let angle = map(i, 0, floor(geno.numLines), 0, TWO_PI); let baseRadius = min(simW, simH) * 0.35; let xOff = map(cos(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5); let yOff = map(sin(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5); let noiseVal = noise(xOff + geno.baseHue * 0.01, yOff + geno.strokeW * 0.1, time * 0.2 + geno.strokeW * 0.05); let r_dev = map(noiseVal, 0, 1, -min(simW,simH) * 0.1 * geno.noiseScale * 10, min(simW,simH) * 0.1 * geno.noiseScale * 10); let r = baseRadius + r_dev; let x_unrotated = r * cos(angle); let y_unrotated = r * sin(angle); let x_rotated = x_unrotated * cos(rotation) - y_unrotated * sin(rotation); let y_rotated = x_unrotated * sin(rotation) + y_unrotated * cos(rotation); vertices.push({ x: x_rotated, y: y_rotated }); } return vertices; } function calculateFitness(genotype, targetShape) { if (targetShape === 'random') return random(0.5, 1.5); // Give some variance for random let points = getPhenotypeVertices(genotype); if (points.length < 3) return 0.001; // Minimal fitness for invalid shapes let fitness = 0; if (targetShape === 'circle') { let cx = 0, cy = 0; points.forEach(p => { cx += p.x; cy += p.y; }); cx /= points.length; cy /= points.length; let distances = points.map(p => dist(cx, cy, p.x, p.y)); let meanDistance = distances.reduce((sum, d) => sum + d, 0) / distances.length; if (meanDistance < 1) return 0.001; let variance = distances.map(d => Math.pow(d - meanDistance, 2)).reduce((sum, sq) => sum + sq, 0) / distances.length; let stdDev = Math.sqrt(variance); let relativeStdDev = stdDev / meanDistance; // Adjusted fitness scaling for slower evolution if (relativeStdDev < 0.005) { fitness = 20.0; } // Very good circle else if (relativeStdDev < 0.02) { fitness = 5.0 + (0.02 - relativeStdDev) * (15.0 / 0.015); } else if (relativeStdDev < 0.08) { fitness = 1.0 + (0.08 - relativeStdDev) * (4.0 / 0.06); } else if (relativeStdDev < 0.3) { fitness = 0.2 + (0.3 - relativeStdDev) * (0.8 / 0.22); } else { fitness = 0.05 + Math.max(0, 0.15 - relativeStdDev); } // Low base fitness *= (1 + Math.min(0.5, genotype.numLines / 300)); // numLines bonus capped and scaled down fitness *= (1 / (1 + genotype.noiseScale * 10)); // Stronger penalty for high noise } else if (targetShape === 'square') { fitness = (1 / (1 + Math.abs(genotype.shapeComplexity - 4) * 2)) * 1.0; // Gentler complexity penalty fitness *= (1 / (1 + genotype.noiseScale * 15)); // Stronger noise penalty if(genotype.numLines > 15 && genotype.numLines < 70) fitness *=1.05; // Smaller bonus } else if (targetShape === 'triangle') { fitness = (1 / (1 + Math.abs(genotype.shapeComplexity - 3) * 2)) * 1.0; fitness *= (1 / (1 + genotype.noiseScale * 15)); if(genotype.numLines > 10 && genotype.numLines < 50) fitness *=1.05; } return Math.max(0.001, fitness); } // --- Automatic Evolution Control --- async function startEvolutionCycle(numGenerations) { if (autoEvolving) return; autoEvolving = true; startAutoEvolutionButton.disabled = true; targetShapeSelector.disabled = true; continueEvolutionButton.style.display = 'none'; for (let i = 0; i < numGenerations; i++) { if (!autoEvolving) { autoEvolutionStatusP.innerText = "Evolution stopped by reset."; return; } await evolveOneGenerationAutomatically(); currentAutoEvolutionStep++; autoEvolutionStatusP.innerText = <code>Evolving... Cycle: ${currentAutoEvolutionStep}/${numGenerations} (Total Gen: ${generationCount})</code>; await new Promise(resolve => setTimeout(resolve, 60)); // Slightly longer delay for visibility } autoEvolving = false; if (population.length > 0) { continueEvolutionButton.style.display = 'inline-block'; continueEvolutionButton.disabled = false; } targetShapeSelector.disabled = false; autoEvolutionStatusP.innerText = <code>Cycle finished. Total Gens: ${generationCount}. Continue or Reset.</code>; currentAutoEvolutionStep = 0; } async function evolveOneGenerationAutomatically() { let fitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape)); let parentsPool = selectParentsByFitness(population, fitnessScores, INTERNAL_POP_SIZE); let newPopulation = []; for(let i=0; i < INTERNAL_POP_SIZE; i++) { let p1 = random(parentsPool); let p2 = random(parentsPool); if(!p1 && population.length > 0) p1 = population[0]; else if (!p1) p1 = new Genotype(); // Robust fallback if(!p2 && population.length > 0) p2 = population[0]; else if (!p2) p2 = new Genotype(); // Robust fallback let childGenes = crossover(p1, p2); let mutatedChildGenes = mutate(childGenes); newPopulation.push(mutatedChildGenes); } population = newPopulation; generationCount++; updateGenerationCounter(); let currentGenFitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape)); let bestFitness = -Infinity; let bestIndividual = null; if (population.length > 0) { for (let i = 0; i < population.length; i++) { if (currentGenFitnessScores[i] > bestFitness) { bestFitness = currentGenFitnessScores[i]; bestIndividual = population[i]; } } updateBestIndividualDisplay(bestIndividual, generationCount, false, bestFitness); } displayPopulationSubset(); } function selectParentsByFitness(currentPopulation, fitnessScores, numToSelect) { let selectedParents = []; if(currentPopulation.length === 0) return selectedParents; for (let n = 0; n < numToSelect; n++) { let bestContestant = null; let bestFitnessInTournament = -Infinity; // Renamed for clarity for (let i = 0; i < TOURNAMENT_SIZE; i++) { let randomIndex = floor(random(currentPopulation.length)); if (fitnessScores[randomIndex] > bestFitnessInTournament) { bestFitnessInTournament = fitnessScores[randomIndex]; bestContestant = currentPopulation[randomIndex]; } } if (bestContestant) { selectedParents.push(bestContestant); } else { selectedParents.push(random(currentPopulation)); // Fallback } } return selectedParents; } function crossover(geno1, geno2) { let childData = {}; let g1 = geno1; let g2 = geno2; childData.numLines = random() < 0.5 ? g1.numLines : g2.numLines; childData.baseHue = random() < 0.5 ? g1.baseHue : g2.baseHue; childData.strokeW = random() < 0.5 ? g1.strokeW : g2.strokeW; childData.noiseScale = random() < 0.5 ? g1.noiseScale : g2.noiseScale; childData.shapeComplexity = random() < 0.5 ? g1.shapeComplexity : g2.shapeComplexity; childData.rotationSpeed = random() < 0.5 ? g1.rotationSpeed : g2.rotationSpeed; return new Genotype(childData); } function mutate(geno) { let mutatedData = { ...geno }; const strength = MUTATION_STRENGTH; // Use the global constant if (random() < MUTATION_RATE) { mutatedData.numLines = floor(max(10, mutatedData.numLines + randomGaussian(0, 10 * strength) * 5)); } if (random() < MUTATION_RATE) { mutatedData.baseHue = (mutatedData.baseHue + randomGaussian(0, 30 * strength) * 1.5 + 360) % 360; } if (random() < MUTATION_RATE) { mutatedData.strokeW = max(0.5, mutatedData.strokeW + randomGaussian(0, 0.3 * strength) * 1.5); } if (random() < MUTATION_RATE) { let noiseChange = randomGaussian(0, 0.05 * strength) * 2; mutatedData.noiseScale = max(0.0001, mutatedData.noiseScale + noiseChange); } if (random() < MUTATION_RATE) { mutatedData.shapeComplexity = max(1, mutatedData.shapeComplexity + randomGaussian(0, 1 * strength) * 1.5); } if (random() < MUTATION_RATE) { mutatedData.rotationSpeed = mutatedData.rotationSpeed + randomGaussian(0, 0.003 * strength) * 1.5; } return new Genotype(mutatedData); } // --- User Interaction (for inspecting individuals when not auto-evolving) --- function mousePressed() { if (!autoEvolving && mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { let c = floor(mouseX / cellWidth); let r = floor(mouseY / cellHeight); let gridIndex = c + r * cols; if (gridIndex < POP_SIZE_DISPLAY && gridIndex < population.length) { let actualIndividual = population[gridIndex]; let itsFitness = calculateFitness(actualIndividual, currentTargetShape); updateBestIndividualDisplay(actualIndividual, generationCount, true, itsFitness); } } } function updateGenerationCounter() { generationCounterP.innerText = <code>Generation: ${generationCount}</code>; } function formatGeneValue(value, precision = 2) { if (typeof value === 'number') { return value.toFixed(precision); } return String(value); } function updateBestIndividualDisplay(individual, gen, fromClick = false, fitness = -1) { if (!currentBestInfoDiv || !currentBestImg) return; if (!individual) { currentBestInfoDiv.innerHTML = <code></p> <p> </p> <h4>Best of Gen (Gen ${gen})</h4> <p> </p> <p>No individual data.</p> <p> </p> <p></code>; currentBestImg.style.display = 'none'; return; } let titlePrefix = fromClick ? "Clicked Individual" : "Best of Generation"; let fitnessText = fitness > -Infinity ? <code></p> <p> </p> <p>Fitness: ${formatGeneValue(fitness, 3)}</p> <p> </p> <p></code> : ""; // Show fitness if available currentBestInfoDiv.innerHTML = <code></p> <p> </p> <h4>${titlePrefix} (Gen ${gen})</h4> <p> </p> <p> ${fitnessText} </p> <p> </p> <p>Lines: ${formatGeneValue(individual.numLines, 0)}</p> <p> </p> <p> </p> <p> </p> <p>Hue: ${formatGeneValue(individual.baseHue, 1)}</p> <p> </p> <p> </p> <p> </p> <p>StrokeW: ${formatGeneValue(individual.strokeW, 2)}</p> <p> </p> <p> </p> <p> </p> <p>NoiseSc: ${formatGeneValue(individual.noiseScale, 3)}</p> <p> </p> <p> </p> <p> </p> <p>Complexity: ${formatGeneValue(individual.shapeComplexity, 1)}</p> <p> </p> <p> </p> <p> </p> <p>RotSpeed: ${formatGeneValue(individual.rotationSpeed, 3)}</p> <p> </p> <p></code>; drawPhenotype(individual, pgBest.width / 2, pgBest.height / 2, pgBest.width * 0.9, pgBest.height * 0.9, pgBest); currentBestImg.src = pgBest.canvas.toDataURL(); currentBestImg.style.display = 'block'; } </script>
進化的アート簡易版を実際にやってみて感じたこと
この進化的アート簡易版の特徴は、「世代を重ねるごとに新しいものが生まれる面白さ」です。
最初はランダムな線の集合だったものが、何世代か進むうちに、幾何学的に整ったパターンへと進化していきます。
また、パラメータ(遺伝子)を微調整することで全体の雰囲気ががらっと変わるため、「進化」を観察する楽しみがあります。
進化とは、より良いものを生み出すだけでなく、意外性のある楽しみもあるなと実感しました。
進化的アート × 生成AIの未来
このように、簡単な進化的アルゴリズムで、アートの基礎を生み出すことができます。
Stable Diffusionなどの大規模AIモデルがなくても、自分のパソコンとブラウザさえあれば、進化的アートの世界に入ることができます。
一見するととっつきにくい文言ですが、生成AIなどを使って質問回答を繰り返していけば、誰でも制作は可能です。
今後は、こうした技術が教育現場などでも活用されるでしょう。
進化的アルゴリズム簡易版でアート制作の基礎を試す 終わりに
進化的アルゴリズムは「特別なAIの技術」ではなく、誰もが気軽に扱える創造のツールです。
今回のように簡単なコードを使えば、誰でも「進化するアートの基礎」を作ることができます。
難しいAIの理論やモデル構築に頼らずとも、AIを使えば、新しい表現が生まれてくる。
みなさんも新しいアートを創造してみませんか。
****************
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
****************