シンギュラリティ実験ノート

購入した1000ドルPCで何がどこまでできるのか実験した記録です。

鳥の群れの動きをシミュレーションするプログラムをAIに作らせてみた

5月26日の朝日新聞「スパコン使いこなすAI」という記事を読んだ。1年ほど前、理化学研究所の研究員がChatGPT(当時はGPT4)に、「スパコンを活用して新型コロナのパンデミックを抑えたい。どんな研究が効果的だろう?」と聞いたところ、「飛沫が舞うシミュレーション研究」を提案し、更に具体的なシミュレーション手法を提案したり、プログラムコードのサンプルも作成してくれたという内容だった。

何か新しいことをAIに頼んで見ようと思っていた時にこの記事を読み、10年程前に読んだ本「RとRubyによるデータ解析入門」の中に、鳥の群れの動きをシミュレーションするプログラムがあったのを思い出した。当時この本を読んだ時は、おもしろそうだが内容が難しくて動かすことができなかった。試しにこのテーマでAIにPythonのプログラムの作成を依頼することにした。

Sakana AI 7b(70億パラメタ)

手始めにSakana AIの70億パラメタに頼んでみた。Sakana AIを選んだのは、数学の能力を強化したモデルということでこのテーマに相応しいと思ったからだ。

依頼したプロンプトは以下の通り。

Pythonを使って、鳥の群れの動きをシミュレーションするプログラムをコーディングしてください。
グレッグレイノルズの開発したBoidsの手法を使い、分離(separate)、連帯(align)、凝集(cohere)のルールを組み込み、画面上に鳥の群れの動きが表示できるようにしてください。

Boidsの手法と言われてもほとんどの人は知らないと思う。私も説明はできないので、参考情報に書いたネットの記事を見るか、上記の本を読んでいただきたい。

この程度の依頼(プロンプト)でまともなコードを返してくれるのかに興味があった。Sakana AIが返してくれたコードは以下の通り。

import pygame
from pygame.locals import *

class Bird:
    def __init__(self, x, y):
        self.position = (x, y)
        self.velocity = (0, 0)
        self.acceleration = (0, 0)
        self.radius = 4
        self.color = (255, 0, 0)

    def update(self):
        self.velocity += self.acceleration
        self.position += self.velocity

    def draw(self, surface):
        pygame.draw.circle(surface, self.color, self.position, self.radius)

class Boids:
    def __init__(self, num_birds, width, height):
        self.num_birds = num_birds
        self.width = width
        self.height = height
        self.birds = [Bird(random.randint(0, width), random.randint(0, height)) for _ in range(num_birds)]
        self.flock_center = (width / 2, height / 2)

    def update(self):
        for bird in self.birds:
            bird.update()

    def apply_rules(self, separation, alignment, cohesion):
        for bird in self.birds:
            separation(bird)
            alignment(bird)
            cohesion(bird)

    def draw(self, surface):
        for bird in self.birds:
            bird.draw(surface)

def boids_simulation(width=800, height=600, num_birds=50):
    pygame.init()
    screen = pygame.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
    clock = pygame.time.Clock()
    
    flock = Boids(num_birds, width, height)
    
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                return
                
        flock.update()
        
        screen.fill((255, 255, 255))
        flock.draw(screen)
        
        pygame.display.flip()
        clock.tick(60)

def separation(bird):
    other_birds = [b for b in flock.birds if b != bird]
    separation_force = (0, 0)
    for other_bird in other_birds:
        d = distance(bird.position, other_bird.position)
        if d < 10:
            direction = (other_bird.position[0] - bird.position[0], other_bird.position[1] - bird.position[1])
            separation_force += (direction / d**2)
    
    bird.acceleration += separation_force
    
def alignment(bird):
    neighbors = [b for b in flock.birds if distance(bird.position, b.position) < 50]
    alignment_force = (0, 0)
    for neighbor in neighbors:
        direction = (neighbor.position[0] - bird.position[0], neighbor.position[1] - bird.position[1])
        alignment_force += (direction / len(neighbors))
    
    bird.acceleration += alignment_force
    
def cohesion(bird):
    cohesion_force = (0, 0)
    cohesion_force += (flock.flock_center[0] - bird.position[0], flock.flock_center[1] - bird.position[1])
    
    bird.acceleration += cohesion_force
    
if __name__ == '__main__':
    boids_simulation()
    

依頼した内容をほぼ理解してコーディングしてくれているようだ。pygameライブラリで画面に表示しようとしているので、pip installでpygameをインストールする。しかしその後でコードを実行しても残念ながら動かなかった。以下の基本的ミスを修正すると画面は表示されるが、鳥の印(点)は表示されなかった。

  • import randomを追加
  • 変数position、velosity、accerationがタプルとして宣言されているので、+=で演算するとリストに追加されるだけになるため、numpy配列に変更する

pygameの仕様を確認して修正を試みたが何も表示されない。一旦あきらめて別のAIで試してみることにした。

Llama3 70b(700億パラメタ)

Gloqで実行できるLlama3 70bは、以前に株価の指数関数カーブフィッティングのコーディングを試した時は、ほぼほぼ正しいコードを返してくれたが今回はどうだろう。Sakana AIと同じプロンプトで頼んでみた。

Llama3 70bが返してくれたコードは以下の通り。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Define the number of birds in the flock
num_birds = 100

# Define the initial positions and velocities of the birds
positions = np.random.rand(num_birds, 2) * 100
velocities = np.random.rand(num_birds, 2) * 2 - 1

# Define the Boids algorithm parameters
alignment_weight = 0.125
cohesion_weight = 0.05
separation_weight = 0.025
max_speed = 3

# Define the update function for the Boids algorithm
def update_boids(positions, velocities):
    # Calculate the average velocity and position of the flock
    avg_velocity = np.mean(velocities, axis=0)
    avg_position = np.mean(positions, axis=0)

    # Calculate the alignment, cohesion, and separation forces
    alignment_force = (avg_velocity - velocities) * alignment_weight
    cohesion_force = (avg_position - positions) * cohesion_weight
    separation_force = (positions - np.expand_dims(positions, 1)) * separation_weight
    separation_force = np.sum(separation_force, axis=1)

    # Update the velocities and positions of the birds
    velocities += alignment_force + cohesion_force + separation_force
    velocities = np.clip(velocities, -max_speed, max_speed)
    positions += velocities

    return positions, velocities

# Create a figure and axis for the animation
fig, ax = plt.subplots()
ax.set_xlim(0, 100)
ax.set_ylim(0, 100)

# Create a scatter plot of the birds
scatter = ax.scatter(positions[:, 0], positions[:, 1])

# Define the animation function
def animate(i):
    global positions, velocities
    positions, velocities = update_boids(positions, velocities)
    scatter.set_offsets(positions)

# Create the animation
ani = animation.FuncAnimation(fig, animate, frames=200, interval=50)

# Show the animation
plt.show()

 

上記コードはそのままで動作した。matplotlibを使い、アニメーションを表示してくれる。分離(separation_weight)、連帯(alignment_weight)、凝集(cohesion_weight)の基本パラメータが定義されており、そのまま実行した。結果は以下の通り。

 

 

点がそれらしく動くのだが、集まる力が強すぎるようで、最後は一点に集まってしまう。パラメータで調整できるのだろうが、調整方法が分からない。いくらパラメータを調整しても集まってしまい、動き回ってくれない。(T_T)
仕方がないので、これも一旦あきらめ、現時点で一番有能なAIであるOpenAIのGPT-4oに頼んでみることにした。

GPT-4o

同じプロンプトで頼んでGPT-4oが返してくれたコードは以下の通り。

import pygame
import numpy as np

# Pygameの初期化
pygame.init()

# 画面サイズの設定
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Boids Simulation")

# ボイド(Boid)クラスの定義
class Boid:
    def __init__(self, position, velocity):
        self.position = np.array(position, dtype='float64')
        self.velocity = np.array(velocity, dtype='float64')
        self.max_speed = 4
        self.max_force = 0.1

    def edges(self):
        if self.position[0] > WIDTH:
            self.position[0] = 0
        elif self.position[0] < 0:
            self.position[0] = WIDTH
        if self.position[1] > HEIGHT:
            self.position[1] = 0
        elif self.position[1] < 0:
            self.position[1] = HEIGHT

    def align(self, boids):
        perception_radius = 50
        steering = np.zeros(2)
        total = 0
        avg_vector = np.zeros(2)
        for boid in boids:
            if np.linalg.norm(boid.position - self.position) < perception_radius:
                avg_vector += boid.velocity
                total += 1
        if total > 0:
            avg_vector /= total
            avg_vector = avg_vector / np.linalg.norm(avg_vector) * self.max_speed
            steering = avg_vector - self.velocity
            if np.linalg.norm(steering) > self.max_force:
                steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def cohere(self, boids):
        perception_radius = 50
        steering = np.zeros(2)
        total = 0
        center_of_mass = np.zeros(2)
        for boid in boids:
            if np.linalg.norm(boid.position - self.position) < perception_radius:
                center_of_mass += boid.position
                total += 1
        if total > 0:
            center_of_mass /= total
            vector_to_com = center_of_mass - self.position
            if np.linalg.norm(vector_to_com) > 0:
                vector_to_com = vector_to_com / np.linalg.norm(vector_to_com) * self.max_speed
            steering = vector_to_com - self.velocity
            if np.linalg.norm(steering) > self.max_force:
                steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def separate(self, boids):
        perception_radius = 25
        steering = np.zeros(2)
        total = 0
        for boid in boids:
            distance = np.linalg.norm(boid.position - self.position)
            if distance < perception_radius and distance > 0:
                diff = self.position - boid.position
                diff /= distance
                steering += diff
                total += 1
        if total > 0:
            steering /= total
        if np.linalg.norm(steering) > 0:
            steering = steering / np.linalg.norm(steering) * self.max_speed
        steering -= self.velocity
        if np.linalg.norm(steering) > self.max_force:
            steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def flock(self, boids):
        alignment = self.align(boids)
        cohesion = self.cohere(boids)
        separation = self.separate(boids)

        self.velocity += alignment
        self.velocity += cohesion
        self.velocity += separation

    def update(self):
        if np.linalg.norm(self.velocity) > self.max_speed:
            self.velocity = self.velocity / np.linalg.norm(self.velocity) * self.max_speed
        self.position += self.velocity

    def show(self, screen):
        pygame.draw.circle(screen, (255, 255, 255), self.position.astype(int), 3)

# ボイド群を初期化
boids = [Boid(position=[np.random.rand() * WIDTH, np.random.rand() * HEIGHT], 
              velocity=[np.random.rand() * 2 - 1, np.random.rand() * 2 - 1]) for _ in range(100)]

# メインループ
running = True
clock = pygame.time.Clock()
while running:
    screen.fill((0, 0, 0))
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    for boid in boids:
        boid.edges()
        boid.flock(boids)
        boid.update()
        boid.show(screen)

    pygame.display.flip()
    clock.tick(60)

pygame.quit()

 

さすがGPT-4oだ。初めから完璧な答えを返してくれた。

 

 

静止画ではこの素晴らしさが伝わらないと思う。動画を取りたいのだがその方法が私には分からない。そこでGPT-4oに追加で依頼した。

画面に表示した動きを動画として保存できるようにしてください。

すると、MP4形式でファイルに動画を保存してくれるコードを作成してくれた。

そのコードで作成した動画がこちらである。

 

 

雑感

簡単なプロンプトでAIに指示することで、シミュレーションプログラムを動かすことができた。頭の良い人を「一を聞いて十を知る」とよく表現するが、「Boidsの手法のシミュレーションプログラム」と依頼するだけで理解してくれた生成AIは、そのレベルに近い気がする。

同時にAIの能力の違いも実感することになった。GPT-4oの完成度の高さを体験すると、それ以外のAIは使う気がしない、というのが現時点の正直な感想だ。

70億パラメタのAIは、おもちゃのように遊びで使うならいいが、仕事では使えないだろう。700億パラメタのLlama3も無料で使い放題であることは魅力だが、9割程度の完成度と覚悟して使う必要がある。現時点で、ある程度高度な事をAIに頼むならGPT-4o一択というところか。

しかしこの図式も現時点の図式であり、半年後や1年後にどんな状況になるのかは全く予測できない。AIのビジネスをする企業には大変だろうが、我々利用者からするとおもしろいところだ。

参考情報

Boids手法によるシミュレーションについては以下のページがある。

note.com

 

Pygameで動かすBoidsモデルシミュレーションのコードがこのページにもある。

developer.mamezou-tech.com

 

「RとRubyによるデータ解析入門」の8章にRubyで動かすBoidsモデルの説明がある。当時はRubyが言語の主流になると思っていてこの本を読んだ。

 

GPT-4oが作成してくれた、動きを動画ファイルに保存するPythonのコード。カレントディレクトリに"boids_simulation.mp4"という動画ファイルを作成する。

import pygame
import numpy as np
import moviepy.editor as mpy

# Pygameの初期化
pygame.init()

# 画面サイズの設定
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Boids Simulation")

# ボイド(Boid)クラスの定義
class Boid:
    def __init__(self, position, velocity):
        self.position = np.array(position, dtype='float64')
        self.velocity = np.array(velocity, dtype='float64')
        self.max_speed = 4
        self.max_force = 0.1

    def edges(self):
        if self.position[0] > WIDTH:
            self.position[0] = 0
        elif self.position[0] < 0:
            self.position[0] = WIDTH
        if self.position[1] > HEIGHT:
            self.position[1] = 0
        elif self.position[1] < 0:
            self.position[1] = HEIGHT

    def align(self, boids):
        perception_radius = 50
        steering = np.zeros(2)
        total = 0
        avg_vector = np.zeros(2)
        for boid in boids:
            if np.linalg.norm(boid.position - self.position) < perception_radius:
                avg_vector += boid.velocity
                total += 1
        if total > 0:
            avg_vector /= total
            avg_vector = avg_vector / np.linalg.norm(avg_vector) * self.max_speed
            steering = avg_vector - self.velocity
            if np.linalg.norm(steering) > self.max_force:
                steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def cohere(self, boids):
        perception_radius = 50
        steering = np.zeros(2)
        total = 0
        center_of_mass = np.zeros(2)
        for boid in boids:
            if np.linalg.norm(boid.position - self.position) < perception_radius:
                center_of_mass += boid.position
                total += 1
        if total > 0:
            center_of_mass /= total
            vector_to_com = center_of_mass - self.position
            if np.linalg.norm(vector_to_com) > 0:
                vector_to_com = vector_to_com / np.linalg.norm(vector_to_com) * self.max_speed
            steering = vector_to_com - self.velocity
            if np.linalg.norm(steering) > self.max_force:
                steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def separate(self, boids):
        perception_radius = 25
        steering = np.zeros(2)
        total = 0
        for boid in boids:
            distance = np.linalg.norm(boid.position - self.position)
            if distance < perception_radius and distance > 0:
                diff = self.position - boid.position
                diff /= distance
                steering += diff
                total += 1
        if total > 0:
            steering /= total
        if np.linalg.norm(steering) > 0:
            steering = steering / np.linalg.norm(steering) * self.max_speed
        steering -= self.velocity
        if np.linalg.norm(steering) > self.max_force:
            steering = steering / np.linalg.norm(steering) * self.max_force
        return steering

    def flock(self, boids):
        alignment = self.align(boids)
        cohesion = self.cohere(boids)
        separation = self.separate(boids)

        self.velocity += alignment
        self.velocity += cohesion
        self.velocity += separation

    def update(self):
        if np.linalg.norm(self.velocity) > self.max_speed:
            self.velocity = self.velocity / np.linalg.norm(self.velocity) * self.max_speed
        self.position += self.velocity

    def show(self, screen):
        pygame.draw.circle(screen, (255, 255, 255), self.position.astype(int), 3)

# ボイド群を初期化
boids = [Boid(position=[np.random.rand() * WIDTH, np.random.rand() * HEIGHT], 
              velocity=[np.random.rand() * 2 - 1, np.random.rand() * 2 - 1]) for _ in range(100)]

# 動画保存用設定
duration = 10  # 動画の長さ(秒)
fps = 30  # フレームレート
total_frames = duration * fps

def make_frame(t):
    frame_idx = int(t * fps)
    screen.fill((0, 0, 0))
    for boid in boids:
        boid.edges()
        boid.flock(boids)
        boid.update()
        boid.show(screen)
    return pygame.surfarray.array3d(screen).transpose([1, 0, 2])

# MoviePyを使って動画を作成
animation = mpy.VideoClip(make_frame, duration=duration)
animation.write_videofile("boids_simulation.mp4", fps=fps)

# Pygameを終了
pygame.quit()