diff --git a/exp/board_detect.py b/exp/board_detect.py
--- a/exp/board_detect.py
+++ b/exp/board_detect.py
@@ -246,6 +246,7 @@ class BoardDetector:
 		return sum(self._hough.scoreLine(p.transform(self._inverseMatrix)) for p in itertools.chain(*lines))
 
 	def detectPosition(self,img):
+		if not self.grid: return None
 		(rows,cols)=self.grid
 		intersections=[[row.intersect(col) for col in cols] for row in rows]
 		position=[[self._detectStoneAt(img,point) for point in row] for row in intersections]
diff --git a/exp/keras/__init__.py b/exp/kerokero/__init__.py
rename from exp/keras/__init__.py
rename to exp/kerokero/__init__.py
diff --git a/exp/keras/prepare_data.py b/exp/kerokero/prepare_data.py
rename from exp/keras/prepare_data.py
rename to exp/kerokero/prepare_data.py
--- a/exp/keras/prepare_data.py
+++ b/exp/kerokero/prepare_data.py
@@ -2,20 +2,22 @@ import os
 import sys
 import re
 import random
+import itertools
 
 import numpy as np
 import cv2 as cv
 
-sys.path.append("../exp")
+sys.path.append("..")
+sys.path.append("../../src")
 from annotations import DataFile,computeBoundingBox,Corners
 from geometry import Line
-from keras.transformation_matrices import getIdentity,getRotation,getTranslation,getScale,getMirroring,getProjection
+from kerokero.transformation_matrices import getIdentity,getRotation,getTranslation,getScale,getMirroring,getProjection
 
 random.seed(361)
 
 
 class Sample:
-	SIDE=256
+	SIDE=224
 
 	def __init__(self,img,grid):
 		self.img=img
@@ -33,7 +35,7 @@ class Sample:
 		m=np.matmul(self._computeCrop(m),m)
 		img=cv.warpPerspective(self.img,m,(self.SIDE,self.SIDE))
 		grid=Corners(c.transform(m) for c in self.grid)
-		Sample(img,grid).show()
+		return (img,list(itertools.chain.from_iterable(grid)))
 
 	def _getCenter(self):
 		(a,b,c,d)=self.grid
@@ -76,9 +78,32 @@ def harvestDir(path):
 	boards=annotations["."]
 	for f in files:
 		img=cv.imread(f.path)
+		img=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
 		for b in boards:
 			sample=Sample(img,b.grid)
-			sample.transform()
+			(img,label)=sample.transform()
+			yield (img,label)
+
+
+def loadDataset(root):
+	testRatio=0.1
+	trainRatio=1-testRatio
+	images=[]
+	labels=[]
+	for d in traverseDirs(root):
+		for (img,label) in harvestDir(d):
+			images.append(img)
+			labels.append(label)
+	n=len(images)
+	keys=list(range(n))
+	random.shuffle(keys)
+	images=[images[k] for k in keys]
+	labels=[labels[k] for k in keys]
+	m=int(n*trainRatio)
+	return (
+		(np.uint8(images[:m]),np.float32(labels[:m])),
+		(np.uint8(images[m:]),np.float32(labels[m:]))
+	)
 
 
 def show(img,filename="x"):
diff --git a/exp/kerokero/test.py b/exp/kerokero/test.py
new file mode 100644
--- /dev/null
+++ b/exp/kerokero/test.py
@@ -0,0 +1,30 @@
+import argparse
+
+import numpy as np
+from keras.models import load_model
+
+from prepare_data import loadDataset,Sample
+from analyzer.epoint import EPoint
+from analyzer.corners import Corners
+
+
+parser=argparse.ArgumentParser()
+parser.add_argument("model")
+parser.add_argument("data_dir")
+args=parser.parse_args()
+
+model=load_model(args.model)
+
+print("loading data...")
+((trainImages,trainLabels),(testImages,testLabels))=loadDataset(args.data_dir)
+print("done")
+
+for img in testImages:
+	label=model.predict(np.reshape(img,(1,224,224)))
+	print(label)
+	points=[]
+	for i in range(4):
+		points.append(EPoint(label[0][i*2],label[0][i*2+1]))
+	corners=Corners(points)
+	sample=Sample(np.uint8(img),corners)
+	sample.show()
diff --git a/exp/keras/train.py b/exp/kerokero/train.py
rename from exp/keras/train.py
rename to exp/kerokero/train.py
--- a/exp/keras/train.py
+++ b/exp/kerokero/train.py
@@ -1,13 +1,25 @@
+import argparse
+
 from keras.layers import Conv2D,Dropout,Dense,Flatten
-from keras.models import Sequential
+from keras.models import Sequential,load_model
+
+from prepare_data import loadDataset
 
 
-model = Sequential([
-	Flatten(input_shape=(96,96)),
+parser=argparse.ArgumentParser()
+parser.add_argument("data_dir")
+parser.add_argument("--load_model")
+parser.add_argument("--save_model",default="/tmp/gogo-{0:03}.h5")
+parser.add_argument("--epochs",type=int,default=100)
+parser.add_argument("--initial_epoch",type=int,default=0)
+args=parser.parse_args()
+
+model=Sequential([
+	Flatten(input_shape=(224,224)),
 	Dense(128, activation="relu"),
 	Dropout(0.1),
 	Dense(64, activation="relu"),
-	Dense(30)
+	Dense(8)
 ])
 
 model.compile(
@@ -15,5 +27,14 @@ model.compile(
 	loss='mse',
 	metrics=['mae','accuracy']
 )
+if args.load_model:
+	model=load_model(args.load_model)
 
-model.fit(X_train,y_train,epochs = 500,batch_size = 128,validation_split = 0.2)
+print("loading data...")
+((trainImages,trainLabels),(testImages,testLabels))=loadDataset(args.data_dir)
+print("done")
+
+for i in range(args.initial_epoch,args.epochs//10):
+	model.fit(trainImages,trainLabels,epochs=(i+1)*10,initial_epoch=i*10,batch_size=128,validation_split=1/9)
+	model.save(args.save_model.format(i+1))
+print(model.evaluate(testImages,testLabels))
diff --git a/exp/keras/transformation_matrices.py b/exp/kerokero/transformation_matrices.py
rename from exp/keras/transformation_matrices.py
rename to exp/kerokero/transformation_matrices.py
--- a/exp/keras/transformation_matrices.py
+++ b/exp/kerokero/transformation_matrices.py
@@ -47,8 +47,8 @@ def getMirroring():
 
 
 def getProjection():
-	dx=random.uniform(-0.001,0.001)
-	dy=random.uniform(-0.001,0.001)
+	dx=random.uniform(-0.0005,0.0005)
+	dy=random.uniform(-0.0005,0.0005)
 	return np.float32([
 		[1,0,0],
 		[0,1,0],