見出し画像

Teachable Machine はクリエイティブ・コーディングの夢を見るか?

クリエイティブ・コーディングでの面白い作品づくりに AI を活用してみようというお話しです。

この記事は「Processing Advent Calendar 2019」 23 日目の記事です。
前日の記事は、コードを公開し始めて変わったことや得られた発見についてまとめられた @takawo さんの「2019年のデイリーコーディングを振り返る」です。

画像1

常に面白い作品を作りたいけど…

私は「アートを生み出す機械」を作るのが夢で、今は表現の引き出しを増やすべく毎日楽しく作品作りを行っています。

毎日違う作品をアップする Twitter ボットなども作っているのですが、日によって『今日の作品は面白かった!』だったり、『なんかイマイチ…』だったりします。

クリエイティブ・コーディングで何かを描くとき、パラメータにランダム性を持たせることで同じコードでも毎回違う結果を出そうとすることありますよね?そしてパラメータの値によっては面白い絵になったり、いまいちつまんない絵になったり。
毎回面白い絵が出るようにとパラメータを調整したりするんですけど、これが地味に時間のかかる作業で、やってるうちに自分で自分の作品に飽きてきて『これ一体何が面白かったんだっけ…?』となったりすることも。

何が面白い絵で何がつまんない絵なのか、それをシステム的に判別できればパラメータ調整もシステムで自動的にできそうな気がするんですが…

画像2

今の時代、AI があるじゃない!

そんな風に漠然と思っていたところ、田所さんの AI に関するスライドを拝見しました。

田所さんのクリエイティブ・コーディング関連資料でいつも勉強させていただいています。https://yoppa.org/

「Teachable Machine」というものを使うと AI 学習データが簡単に作れて p5.js でそれを活用できるですって!?

これはいい!
笑顔と非笑顔をこんな風に判別できるなら、きっと面白い絵とつまんない絵も判別できるはず!

画像10


画像3

Teachable Machine の簡単な使い方

Teachable Machine 早速使ってみましたが、すごくとっつきやすくて思ってたよりも簡単に使えました。

以下があればすぐに始められます。
1.インターネット接続
2.Google アカウント
3.Web ブラウザ

まずは「Get Started」をクリックして開始。

画像40

今回は画像を処理するので「Image Project」を選択します。

画像12

この画面で 3つの工程を進めていくだけで学習モデルの作成ができてしまいます。

画像13

それぞれの工程のチュートリアル動画があり、これを見ると使い方はすぐにわかります。

画像14


チュートリアル動画は左上のハンバーガーメニューから何回でも見ることが出来ます。

画像15


画像4

モデルデータを作成してみる

Teachable Machine の使い方を把握するため、まずは丸と四角の判定ができるかやってみます。

Processing を使って円を複数描画した画像と、正方形を複数描画した画像を学習データとしてそれぞれ 500個ずつ用意しました。
縦長や横長の画像は Teachable Machine で正方形に切り取られてしまうので、縦横比が 1:1 の画像にしましょう。

画像40

最初の工程として用意した画像ファイルの読み込みを行います。
ここでは「Upload」をクリックし複数の画像を一度にアップロードすることができます。

画像17

それぞれの Class名は、鉛筆マークをクリックして Circle と Square に変更しておきました。

画像ファイルの読み込みが終わったら、2番目の工程としてトレーニングを行います。

画像18

画像19

トレーニングが終わったら、上手く判定できるかプレビューできます。

画像20

デフォルトでは Web カメラからの入力になっているので、ファイルに変更して試してみます。

画像21

学習データとは別に、新たな画像を作成して試しました。うん!上手く判定されるようです。

画像22

画像23


最後の工程として、出来上がったモデルデータを「Export Model」でエクスポートします。

画像24

モデルデータはダウンロードも出来るのですが、ダウンロードしたデータを JavaScript で使うには CORS(Cross Origin Resourse Sharing) の問題が面倒なのでアップロードを選択しました。


こんな風に「Your sharable link」にモデルデータのリンクが生成されますので、これを後で使うためにコピーしておきます。

画像25

モデルデータを使うための p5.js コードの雛形もここに表示されますので、これも後で使うためにコピーしましょう。

画像26

これでモデルデータ作成は終了です。
プロジェクトは自分の Google ドライブに保存しておけます。ハンバーガーメニューの「Save project to Drive」から保存しておきましょう。

画像27


画像5

モデルデータを活用するコードを書く

先程コピーした p5.js の雛形を加工して、自分で描画したイメージを自分で判定するというコードを書いてみます。

その前にまず、雛形のコードを見てみましょう。

<div>Teachable Machine Image Model - p5.js and ml5.js</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
<script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>
<script type="text/javascript">
 // Classifier Variable
 let classifier;
 // Model URL
 let imageModelURL = './my_model/';
 
 // Video
 let video;
 let flippedVideo;
 // To store the classification
 let label = "";

 // Load the model first
 function preload() {
   classifier = ml5.imageClassifier(imageModelURL + 'model.json');
 }

 function setup() {
   createCanvas(320, 260);
   // Create the video
   video = createCapture(VIDEO);
   video.size(320, 240);
   video.hide();

   flippedVideo = ml5.flipImage(video)
   // Start classifying
   classifyVideo();
 }

 function draw() {
   background(0);
   // Draw the video
   image(flippedVideo, 0, 0);

   // Draw the label
   fill(255);
   textSize(16);
   textAlign(CENTER);
   text(label, width / 2, height - 4);
 }

 // Get a prediction for the current video frame
 function classifyVideo() {
   flippedVideo = ml5.flipImage(video)
   classifier.classify(flippedVideo, gotResult);
 }

 // When we get a result
 function gotResult(error, results) {
   // If there is an error
   if (error) {
     console.error(error);
     return;
   }
   // The results are in an array ordered by confidence.
   // console.log(results[0]);
   label = results[0].label;
   // Classifiy again!
   classifyVideo();
 }
</script>

この雛形は、ざっとこのような動作をしています。

1. preload() でモデルデータを元に ml5.js の判定器 classifier を作成。
2. setup() で Web カメラの画像を入力として classifyVideo() で判定を開始。
3. 判定は classifier.classify() で非同期に実行され、結果が得られ次第 gotResult() が起動される。
4. gotResult() では判定結果を label にセットし、 classifyVideo() で再び判定を開始。
5. draw() では Web カメラの画像を描画し、その上に判定結果の label を text() で描画。

非同期の classifyVideo() → gotResult() を繰り返す判定ループと、通常の draw() の描画ループの 2つのループがぐるぐる回ってるという動きです。

余談:判定が非同期だから、描画された Web カメラの画像とその判定結果が一致していない場合も有り得る気がします。

大きな流れはこのままに、下記の変更を加えます。

1. Web カメラの入力を、p5.js で描画した createGraphics() に変更
2. 判定ループを有限回数にして、全ての判定が完了した時点で結果を描画する

で、出来たコードがこちら。

<!doctype html>
<html lang="ja">
 <head>
   <!-- Apache License Version 2.0 http://www.apache.org/licenses/ -->
   <meta charset="utf-8" />
   <title>Image Examiner.</title>
 <head>
   <body>
     <h1>Image Examiner.</h1>
     <h2>Teachable Machine Image Model - p5.js and ml5.js</h2>
     <div id="results"></div>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
     <script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>
     <script type="text/javascript">

	const examMax = 9;
	const circleImg = new Array();
	const squareImg = new Array();
	
	// Classifier Variable
	let classifier;
	// Model URL
	let imageModelURL = 'https://teachablemachine.withgoogle.com/models/eVrCahzG/';
	
	// Video
	let flippedVideo;

	// Load the model first
	function preload() {
	    classifier = ml5.imageClassifier(imageModelURL + 'model.json');
	    console.log('preload');
	}

	function setup() {
	    colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    frameRate(1);
	    // Start classifying
	    classifyVideo();
	    console.log('setup');
	}

	function draw() {
	    const canvas = createCanvas(1200, 640);
	    canvas.parent('results');
	    canvas.background(0, 0, 0, 100);
	    noStroke();
	    textSize(20);
	    textAlign(CENTER);
	    // Draw the results
	    if (circleImg.length + squareImg.length >= examMax) {
		fill(200, 60, 80, 100);
		rect(0, 0, 600, 40);
		fill(0, 0, 100, 100);
		text('Circle', 300, 20);
		image(drawResults(circleImg), 0, 40);

		fill(10, 60, 80, 100);
		rect(600, 0, 600, 40);
		fill(0, 0, 100, 100);
		text('Square', 900, 20);
		image(drawResults(squareImg), 600, 40);

		noLoop();
		console.log(frameCount, 'results');
	    }
	    console.log(frameCount, 'draw');
	}

	
	function drawResults(_ary) {
	    const img = createGraphics(600, 600);
	    img.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    img.background(0, 0, 90, 100);
	    if (_ary.length != 0) {
		for (let i = 0; i < _ary.length; i++) {
		    img.image(_ary[i], 1 + (i % 3) * 200, 1 + floor(i / 3) * 200, 198, 198);
		}
	    }
	    return img;
	}

	// Get a prediction for the current video frame
	function classifyVideo() {
	    flippedVideo = examinee(random(1, 5));
	    classifier.classify(flippedVideo, gotResult);
	}

	// When we get a result
	function gotResult(error, results) {
	    // If there is an error
	    if (error) {
		console.error(error);
		return;
	    }
	    // The results are in an array ordered by confidence.
	    const label = results[0].label;
	    console.log(frameCount, label);

	    if (label == 'Circle') {
		evalCircle();
	    } else {
		evalSquare();
	    }

	    if (circleImg.length + squareImg.length < examMax) {
		// Classifiy again!
		classifyVideo();
	    }
	}

	function evalCircle() {
	    circleImg .push(flippedVideo);
	}

	function evalSquare() {
	    squareImg.push(flippedVideo);
	}

	function examinee(_num) {
	    let baseHue = random(360.0);
	    let drawFunc = '';
	    if (random(1.0) < 0.5) {
		drawFunc = function(_i, _x, _y, _s) {_i.ellipse(_x, _y, _s, _s);}
	    } else {
		drawFunc = function(_i, _x, _y, _s) {_i.rect(_x, _y, _s, _s);}
	    }
	    
	    const img = createGraphics(480, 480);
	    img.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    img.background(0, 0, 100, 100);
	    for (let i = 0; i < _num; i++) {
		let ratio = map(i, 0, _num, 0.0, 1.0);
		let eSize = ratio * img.width;
		img.fill((baseHue + ratio * 90.0), 40.0, 80.0, 100.0);
		img.stroke((baseHue + ratio * 90.0), 40.0, 80.0, 100.0);
		drawFunc(img, random(img.width), random(img.height), eSize);
	    }
	    return img;
	}

     </script>
   </body>
</html>

モデルデータは誰でも使える状態なので、このコードで実際に動作を試していただけると思います。
ライセンスは Teachable Machine と同じく Apache License Version 2.0 にしました。

元の雛形との比較が容易になるよう、変数の名前はできるだけ元のまま残してあります。(決してリファクタリングをサボってるわけではありません)
各所に console.log() を入れましたので、コンソールを見ていただければ動きを追っていただけると思います。

実行結果はこちらです。バッチリですね!

画像28

円や正方形を 1/5 程度に小さくしたり、数を 5倍程度に増やしてもうまく判定してくれています。

画像29


画像6

Teachable Machine にちょっと意地悪

ここで Teachable Machine にちょっと意地悪して正方形の代わりに三角形を与えてみましょう。

// drawFunc = function(_i, _x, _y, _s) {_i.rect(_x, _y, _s, _s);}
   drawFunc = function(_i, _x, _y, _s) {_i.triangle(_x, _y, _x + _s, _y, _x, _y + _s);}

おやおや、見事に正方形扱いです。

画像30

これは三角形の直角部分が正方形と同じ角度だからかもしれませんね。
三角形の角度を変えてみましょう。

  drawFunc = function(_i, _x, _y, _s) {_i.triangle(_x, _y, _x + _s, _y + _s, _x - _s, _y + _s);}

お!判定が分かれました!

画像31


画像7

判定の信頼度を反映させる

判定結果として得られるデータは多次元配列になっており、今まではその中の label だけを見ていました。多次元配列の中には判定の信頼度の値が入る confidence という項目があります。

コンソールに判定結果を全て表示してみると三角形の判定信頼度が低いのがわかります。

console.log(frameCount, results);

// 円の判定結果例
1 (2) […]
0: Object { label: "Circle", confidence: 0.9970993399620056 }
1: Object { label: "Square", confidence: 0.0029007112607359886 }
length: 2
<prototype>: Array []

// 三角形の判定結果例
1 (2) […]
0: Object { label: "Square", confidence: 0.6159985065460205 }
1: Object { label: "Circle", confidence: 0.3840014338493347 }
length: 2
<prototype>: Array []

円の判定結果が 99%超えで自信満々なのに対して、三角形は「62% ぐらいで正方形だと思うんだけど…」という感じですね。

信頼度が低い判定結果は「どちらでもない」という扱いにコードを変えてみましょう。信頼度が 0.99 より大きい場合のみ結果を信用するようにしました。

   if (confidence > 0.99) {
	if (label == 'Circle') {
	    evalCircle();
	} else {
	    evalSquare();
	}
   } else {
	evalNeither();
   }

画像32

うまく判定できたようです!


画像8

Teachable Machine で面白い絵を判別できるか?

これで仕組みは整いました。さあいよいよ本題! Teachable Machine を使って面白い絵を判別できるでしょうか?
やってみましょう!

面白い絵とそうでもない絵のモデルデータ生成には、数式を自動生成して描画するシステムを使いました。

これを使ってサンプル画像は大量に作れるのですが、これを一つ一つ確認して面白い絵とそうでもない絵に仕分けていかなきゃいけない…
こ、これはしんどい…

やってるうちにどれが面白い絵なのかわからなくなってくる… これどれもつまんなくない?

面白い絵とつまんない絵を仕分ける仕組みがあればなぁ……いやいや、それを今やってるんじゃないか。
しかし、つまんない絵ばっかり溜まっていくな。あっ!!仕分けたやつ消してしまった!ああ!ぁああおおああああ!😫

と、楽しく作業を続けた結果、面白い絵 410個、つまんない絵 700個が用意できました。😅

画像33

これを元にモデルデータを生成! プレビューで 100% Bad! 迷いなき判定! OK!

画像34

p5.js で画像を生成して判定させるコードも書けました。

<!doctype html>
<html lang="ja">
 <head>
   <!-- Apache License Version 2.0 http://www.apache.org/licenses/ -->
   <meta charset="utf-8" />
   <title>Image Examiner.</title>
   <style>
   </style>
 <head>
   <body>
     <h1>Image Examiner.</h1>
     <h2>Teachable Machine Image Model - p5.js and ml5.js</h2>
     <div id="results"></div>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
     <script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>
     <script type="text/javascript">

	const examMax = 12;
	const goodImg = new Array();
	const badImg = new Array();
	const neitherImg = new Array();
	
	// Classifier Variable
	let classifier;
	// Model URL
	let imageModelURL = 'https://teachablemachine.withgoogle.com/models/-3hZp0di/';
	
	// Video
	let flippedVideo;

	// Load the model first
	function preload() {
	    classifier = ml5.imageClassifier(imageModelURL + 'model.json');
	    console.log('preload');
	}

	function setup() {
	    colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    frameRate(1);
	    // Start classifying
	    classifyVideo();
	    console.log('setup');
	}

	function draw() {
	    const canvas = createCanvas(1800, 640);
	    canvas.parent('results');
	    canvas.background(0, 0, 0, 100);
	    noStroke();
	    textSize(20);
	    textAlign(CENTER);
	    // Draw the results
	    if (goodImg.length + badImg.length + neitherImg.length >= examMax) {
		fill(200, 60, 80, 100);
		rect(0, 0, 600, 40);
		fill(0, 0, 100, 100);
		text('Excellent!', 300, 20);
		image(drawResults(goodImg), 0, 40);

		fill(120, 40, 60, 100);
		rect(600, 0, 600, 40);
		fill(0, 0, 100, 100);
		text('Not bad.', 900, 20);
		image(drawResults(neitherImg), 600, 40);

		fill(10, 60, 80, 100);
		rect(1200, 0, 600, 40);
		fill(0, 0, 100, 100);
		text('So so.', 1500, 20);
		image(drawResults(badImg), 1200, 40);

		noLoop();
		console.log(frameCount, 'results');
	    }
	    console.log(frameCount, 'draw');
	}

	
	function drawResults(_ary) {
	    const img = createGraphics(600, 600);
	    img.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    img.background(0, 0, 90, 100);
	    if (_ary.length != 0) {
		for (let i = 0; i < _ary.length; i++) {
		    img.image(_ary[i], 1 + (i % 3) * 200, 1 + floor(i / 3) * 200, 198, 198);
		}
	    }
	    return img;
	}

	// Get a prediction for the current video frame
	function classifyVideo() {
	    flippedVideo = examinee(500);
	    classifier.classify(flippedVideo, gotResult);
	}

	// When we get a result
	function gotResult(error, results) {
	    // If there is an error
	    if (error) {
		console.error(error);
		return;
	    }
	    // The results are in an array ordered by confidence.
	    const label = results[0].label;
	    const confidence = results[0].confidence;
	    console.log(frameCount, label, confidence);

	    // label may be an index number occasionally.
	    if (label == 'Good' || label == 0) {
		if (confidence > 0.95) {
		    evalGood();
		} else {
		    evalNeither();
		}
	    } else {
		if (confidence > 0.85) {
		    evalBad();
		} else {
		    evalNeither();
		}
	    }

	    if (goodImg.length + badImg.length + neitherImg.length < examMax) {
		// Classifiy again!
		classifyVideo();
	    }
	}

	function evalGood() {
	    goodImg .push(flippedVideo);
	}

	function evalBad() {
	    badImg.push(flippedVideo);
	}

	function evalNeither() {
	    neitherImg.push(flippedVideo);
	}

	function examinee(_num) {

	    const hueBase  = random(360.0);
 	    const plotDiv  = random(0.0001, 0.002);
 	    const plotMult = random(1.0, 20.0);
	    const plotMax  = floor(random(200, 600));
	    const sizBase = 2.0;

	    const parms = ['_a', '_b'];
	    const oprts = [' + ', ' - ', ' * ', ' % '];
	    const funcs = ['sin({p})', 'cos({p})', 'asin({p})', 'acos({p})', 'atan({p})', 'exp({p})', 'sqrt({p})', 'log({p})', 'pow({p}, 2)', 'pow({p}, -2)'];
	    
	    const arg01 = '_a';
	    const arg02 = '_b';
	    const bodyHead = 'let result = ';
	    const bodyTail = '; return result;';
	    const funcsCnt = floor(random(5.0, 10.0));

	    let formula = funcs[floor(random(funcs.length))].replace(/\{p\}/, parms[floor(random(parms.length))]);
	    for (i = 0; i < funcsCnt; i++) {
		formula += oprts[floor(random(oprts.length))];
		formula += funcs[floor(random(funcs.length))].replace(/\{p\}/, parms[floor(random(parms.length))]);
	    }
	    let calculator = Function(arg01, arg02, bodyHead + formula + bodyTail);
	    
	    
	    const img = createGraphics(480, 480);
	    img.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
	    img.background(0, 0, 100, 100);
	    img.noStroke();
	    for (let xInit = 0.0; xInit < 1.0; xInit += 0.025) {
		for (let yInit = 0.0; yInit < 1.0; yInit += 0.025) {
		    let xPrev = random(0.1, 0.9);
		    let yPrev = random(0.1, 0.9);
		    let xPlot = xPrev;
		    let yPlot = yPrev;
		    for (plotCnt = 0; plotCnt < plotMax; plotCnt++) {
			let plotRatio = map(plotCnt, 0, plotMax, 0.0, 1.0);
     			let pHue = hueBase + plotRatio * 30.0;
     			let pSat = plotRatio * 40.0;
     			let pBri = 30.0 + (1.0 - plotRatio) * 50.0;
     			let pSiz = sin(PI * plotRatio) * sizBase;
			
			let calcVal = calculator(xPrev, yPrev);
     			xPlot += plotDiv * cos(atan2(calcVal, yPrev) * plotMult);
     			yPlot += plotDiv * sin(atan2(calcVal, xPrev) * plotMult);

     			img.fill(pHue % 360.0, pSat, pBri, 30.0);
     			img.ellipse(xPlot * img.width, yPlot * img.height, pSiz, pSiz);
			
			xPrev = xPlot;
     			yPrev = yPlot;
		    }		
		}
	    }
	    return img;
	}

     </script>
   </body>
</html>


このコードを使って新たに生成した画像を判定させてみます。

画像35

うっ!き、厳しいっ!!!
…た、たた、確かにどれもあまり面白い絵ではないですね。ではもう一度…

画像36

むむ、微妙!
つまらない絵と判定されたものの中にも結構いい感じのものがあるようなので、判定信頼度のチェックを調整してみます。

画像37

画像38

画像39

うーん、合ってるような、そうでもないような…。

面白い絵とつまらない絵が完全にバッチリ分けられるというより、「面白い絵の判定結果の方には面白い絵が多い傾向がある」という感じにしかならないようですね。

画像9

やってみた感想まとめ

Teachable Machine の判定結果を何度も見てるうちに、『これは面白いと判定されたから面白い絵なんだ、こっちはつまらない絵なんだ』と思い込もうとする気持ちがふと浮かんできました。

これにはちょっとハッとさせられました。

画像40

「Teachable Machine」って、簡単に教えることが出来る機械って意味じゃなくて、「人間に考えを教え込むことができる機械」って意味だったりして…?

そんなわけないですね。でも、ちょっと大袈裟かもしれませんが『自分の感性が主で機械の判定は補助として使う、機械の判定を私が利用するんだ』という気持ちをしっかり持っていないと、いつの間にか機械の言いなりになってしまいそうな気がしました。


サービスとしての Teachable Machine はとてもわかりやすく、よく整えられた素晴らしいサービスだと思います。
実際に使ってみて「機械学習させてその結果を使う」ということが思っていたよりずっと簡単にできることもわかりました。

今回私が作成したものは「面白い絵の判定機」としては未熟なものでしたが、そこに確かな可能性も感じられました。モデルの質をもっと上げていき、ロジックも工夫すれば十分実用になると思います。

描画したものを判定にかけ、良い絵と判定されたものだけを表示することで毎回違う面白い絵だけを表示するコードも書けるでしょう。遺伝的アルゴリズムと組み合わせれば、面白い絵を描くパラメータを自動で調節することもできそうです。

Twitter の「いいね」の数によってモデルデータの面白い絵とそうでない絵の仕分けをするというアイディアも面白いかもしれません。
表示した絵をインタラクティブに鑑賞者に評価してもらい、それをモデルデータにフィードバックすることで段々と成長して絵が上手くなっていくコードも書けそうです。

私の夢「アートを生み出す機械」の実現は近いっ!やったね!😀


それが「アート」かどうかはまた別の話だけど。😝


明日の「Processing Advent Calendar 2019」@kashibat さんの「クリスマスだしp5.jsで雪片を降らせてみるよ2019」です。
なんと毎年違う手法で雪片を作成する 2017年から続くシリーズ記事です。
お楽しみに!




この記事が面白かったらサポートしていただけませんか? ぜんざい好きな私に、ぜんざいをお腹いっぱい食べさせてほしい。あなたのことを想いながら食べるから、ぜんざいサポートお願いね 💕