進化的アルゴリズム簡易版でアート制作の基礎を試す

進化的アルゴリズム簡易版を試してみる

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/

****************

GoogleのAlphaEvolve(アルファエボルブ)とSakanaAIのDarwin Gödel Machine(ダーウィン・ゲーデルマシン)を参考に簡略化した自己改善型AIシステムで遊んだ話

「進化的アルゴリズムAI」や「自己改善型AI」とは

近年、AI研究の世界でじわじわと注目を集めているキーワードがある。それが「進化的アルゴリズムAI」や「自己改善型AI」だ。

要するに、AIが自分自身のコードや構造を見直して、より賢くなる方法を自分で考え、試し、改善していくというコンセプトである。

これが実現すれば、AIは単なるツールから、ある種の“学び続ける存在”へと進化する可能性を秘めている。

この分野で特に話題になった2つの事例がある。ひとつは、Google DeepMindが開発したAlphaEvolve(アルファエボルブ)。

そしてもうひとつが、Sakana AIによるDarwin Gödel Machine(DGM)だ。

進化的アルゴリズムAIとは

進化的アルゴリズムAIとは、生物の進化の仕組みを応用した人工知能だ。

複数のAI(個体)を用意し、性能の良いものを選んで子を作り、少しずつ変化させながら世代交代を繰り返す。

これにより、最適な解や動作を自動的に進化させていく。
ゲームやロボット制御、デザイン生成など幅広く使われている。

AlphaEvolveは、AI自身が自分のニューラルネットワーク構造を進化させるシステムだ。

従来のモデル構築は人間が試行錯誤する必要があったが、AlphaEvolveは複数のモデル構造を遺伝的に生成し、その中で最も優れたものを選抜・交配していくという、いわば“AIの自然選択”のような仕組みを持っている。

自己改善型AIとは何か?

一方、DGMは少し違う。こちらはAIが自分のPythonコードそのものを読み取り、改善案を考え、自ら書き換えていくという、まるでAIがプログラマーのように振る舞うスタイルをとっている。

LLM(大規模言語モデル)を使って改善案を生成し、それを実行して効果があるかをベンチマークで評価。

効果があれば採用、なければ元に戻す。まさに“自己改善ループ”が構築されているのだ。

自分で作ってみた「ミニDGM」

そんな高度な仕組みを見ていると、「自分でもちょっと遊んでみたい」と思った。

そこで今回は、Sakana AIのDGMのアイデアを参考にしつつ、Google Colab上で動く簡易版の自己改善型AIシステムを作ってみた。

テーマはシンプル。

「数字のリストをソートするプログラムを、GPT-4に自動的に改善させる」というものだ。

初期コードと改善戦略

最初に与えるコードは、あえて非効率な「バブルソート」だった。

しかも time.sleep(0.001) をループ内に入れて、わざと遅くしてある。

def sort_numbers(numbers):
    for i in range(len(numbers)):
        for j in range(len(numbers) - i - 1):
            if numbers[j] > numbers[j+1]:
                numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
            time.sleep(0.001)
    return numbers

これを GPT-4 に渡し、「もっと速くなるように改善して」と頼む。

改善案はColab内で保存し、実際に実行してスピードと正確性でスコアを出す。

改善後のスコアが高ければ採用、そうでなければ前のコードに戻す――という流れだ。

以下Pythonコードです。
ご自由に使ってください。

import os
import importlib.util
import shutil
import yaml
from openai import OpenAI

client = OpenAI()  # APIキーは環境変数 OPENAI_API_KEY から取得

# 評価関数読み込み
def load_benchmark(entry_path, eval_fn_name):
    spec = importlib.util.spec_from_file_location("benchmark", entry_path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return getattr(mod, eval_fn_name)

# 改善案をGPTから取得(プロンプト強化+バグ回避)
def generate_candidate_code(code, score):
    prompt = f"""You are an AI agent improving the following Python code to increase its performance.
The current code scores {score} points. Suggest a better version of the code.

# CODE START
{code}
# CODE END

Return only the improved code. Do not include explanations or comments. 
Ensure the code is syntactically correct and fully executable.
"""
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7
    )
    return response.choices[0].message.content

# メインループ
def main():
    config = yaml.safe_load(open("configs/sort_experiment.yaml"))
    agent_path = config["agent"]["path"]
    benchmark = config["benchmark"]
    evaluator = load_benchmark(benchmark["entry_point"], benchmark["evaluation_function"])

    os.makedirs("output", exist_ok=True)
    shutil.copy(agent_path, "output/agent_0.py")
    current_score = evaluator("output/agent_0.py")
    print(f"[Turn 0] Score = {current_score}")

    for turn in range(1, config["runtime"]["max_turns"] + 1):
        prev_path = f"output/agent_{turn - 1}.py"
        new_path = f"output/agent_{turn}.py"
        log_path = f"output/log_turn_{turn}.txt"

        old_code = open(prev_path).read()
        new_code = generate_candidate_code(old_code, current_score)

        # 保存:改善コードログ
        with open(log_path, "w") as logf:
            logf.write(new_code)

        # 保存:実行用エージェントコード
        with open(new_path, "w") as f:
            f.write(new_code)

        try:
            new_score = evaluator(new_path)
        except Exception as e:
            print(f"[Turn {turn}] ❌ Evaluation error: {e}")
            new_score = 0

        print(f"[Turn {turn}] Score = {new_score}")

        if new_score > current_score:
            print("✅ Improvement accepted.")
            current_score = new_score
        else:
            print("❌ No improvement. Reverting.")
            shutil.copy(prev_path, new_path)
            current_score = evaluator(prev_path)

if __name__ == "__main__":
    main()



GPT-4は何をしたか?

1ターン目でGPT-4は見事に改善に成功した。

バブルソートを numbers.sort() に置き換え、処理時間が大幅に短縮。

ベンチマークスコアは一気に50点から100点に上昇し、コードは無事採用された。


def sort_numbers(numbers):
    numbers.sort()
    return numbers

だが、その後のターンでは改善の余地がないと判断され、スコアは100点のまま変化せず、コードも元に戻された。

興味深かったのは、GPT-4が「関数の重複定義」や「構造を簡素化しすぎる」など、改善案として微妙な方向に進んだケースがあったことだ。

たとえば、main()の中に sort() を直接書いてしまう案などもあり、構文的には正しくても設計上の意図から外れていたりする。

改善の工夫と学び

今回の実験では以下のような工夫が効果的だった。

意図的にコードを非効率にして、改善余地を作る。

スコアの評価関数を調整して、“改善したと判断できる余地”を作る。

GPTの出力をログに保存し、問題が起きたときに原因をすぐ特定できるようにする。

プロンプトを工夫して「重複コードを出さない」「説明文を返さない」よう指示する。

わずか数十行のコードでも、自己改善の流れを構築することで、AIが「考え、試し、評価し、直す」というプロセスを擬似的に再現できたのはかなり面白い体験だった。

今後に向けて

今回の実験は、言ってしまえば超簡略版の自己改善型AIだが、それでも「AIが自分で進化することを考える」というサイクルが働いていることにワクワクを感じた。

AlphaEvolveやDGMのような本格的な自己進化・自己改善型AIシステムは、まだまだ研究の序章だ。

しかし、こうしてそのエッセンスを切り取って再現してみることで、自分なりにその仕組みを体験できるというのは、大きな学びと楽しさがある。

次は、もっと複雑なタスクに自己改善型AIを応用してみたいと思っている。

「AIが自身を育てる」
そんな未来が、もうすぐそこまで来ていると感じた実験だった。


ただ注意しなければならないのは、人間による要チェックを怠ってはいけないということだ。

これはsakanaAIさんのブログ記事を見てもわかるように、AIが自分勝手に都合のいいように改造していく例があったようだ。

ここの所は十分にチェック機構を持たないといけないだろう。

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

PAGE TOP