diff --git a/src/NeuralSolver.cs b/src/NeuralSolver.cs deleted file mode 100644 index 5944012..0000000 --- a/src/NeuralSolver.cs +++ /dev/null @@ -1,198 +0,0 @@ -using Tensorflow; -using Tensorflow.Gradients; -using Tensorflow.Keras.Engine; -using Tensorflow.NumPy; -using static Tensorflow.Binding; -using static Tensorflow.KerasApi; -namespace InverseOfLife; - -public class NeuralSolver -{ - private int Width { get; set; } - private int Height { get; set; } - private int Steps { get; set; } - private bool QuotientX { get; set; } - private bool QuotientY { get; set; } - private IOptimizer Optimizer { get; set; } - - private IModel ForwardModel { get; set; } - private IModel ReverseModel { get; set; } - - private void BuildForwardModel() - { - Tensors inputs = keras.Input(shape: new Shape(Height, Width, 1), name: "InitialState"); - Tensors hidden = keras.layers.Conv2D(32, kernel_size: 3, padding:"same", activation: keras.activations.Relu).Apply(inputs); - hidden = keras.layers.Conv2D(32, kernel_size: 3, padding: "same", activation: keras.activations.Relu).Apply(hidden); - Tensors outputs = keras.layers.Conv2D(1, kernel_size: 1, padding: "same", activation: keras.activations.Sigmoid).Apply(hidden); - ForwardModel = keras.Model(inputs, outputs, name: "ForwardModel"); - } - - private void BuildReverseModel() - { - Tensors inputs = keras.Input(shape: new Shape(Height, Width, 1), name: "FinalState"); - Tensors hidden = keras.layers.Conv2D(32, kernel_size: 3, padding: "same", activation: keras.activations.Relu).Apply(inputs); - hidden = keras.layers.Conv2D(32, kernel_size:3, padding:"same", activation: keras.activations.Relu).Apply(hidden); - Tensors outputs = keras.layers.Conv2D(1, kernel_size: 1, padding:"same", activation: keras.activations.Sigmoid).Apply(hidden); - ReverseModel = keras.Model(inputs, outputs, name: "ReverseModel"); - } - - public NeuralSolver(int width, int height, int steps, bool quotientX, bool quotientY) - { - Width = width; - Height = height; - Steps = steps; - QuotientX = quotientX; - QuotientY = quotientY; - BuildForwardModel(); - BuildReverseModel(); - } - - public void SaveModel(string basePath) - { - ForwardModel.save($"{basePath}/FM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}"); - ReverseModel.save($"{basePath}/RM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}"); - } - public void LoadModel(string basePath) - { - ForwardModel = keras.models.load_model($"{basePath}/FM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}"); - ReverseModel = keras.models.load_model($"{basePath}/RM{Width}x{Height}_{Steps}_{QuotientX}_{QuotientY}"); - } - public (NDArray, NDArray) GenerateTrainingData(int datasetSize) - { - Random rnd = new Random(); - - float[] inputsData = new float[datasetSize * Height * Width]; - float[] labelsData = new float[datasetSize * Height * Width]; - - for (int idx = 0; idx < datasetSize; idx++) - { - Board board = new Board(Width, Height, QuotientX, QuotientY); - int randomCells = rnd.Next(1, Width * Height / 4); - for (int c = 0; c < randomCells; c++) - { - int x = rnd.Next(0, Width); - int y = rnd.Next(0, Height); - board.Toggle(x, y, true); - } - - int offsetLabel = idx * Width * Height; - foreach ( (int x, int y) in board.Lives) - { - int pos = y * Width + x; - labelsData[offsetLabel + pos] = 1f; - } - - board.Evaluate(Steps); - - int offsetInput = idx * Width * Height; - foreach (var (x, y) in board.Lives) - inputsData[offsetInput + (y * Width + x)] = 1f; - } - NDArray inputsTensor = np.array(inputsData).reshape(new Shape(datasetSize, Height, Width, 1)); - NDArray labelsTensor = np.array(labelsData).reshape(new Shape(datasetSize, Height, Width, 1)); - return (inputsTensor, labelsTensor); - } - - public void Train(int datasetSize = 1000, int batchSize = 8, int epochs = 10) - { - (NDArray trainFinal, NDArray trainInitial) = GenerateTrainingData(datasetSize); - Optimizer = keras.optimizers.Adam(learning_rate: 0.001f); - for (int epoch = 0; epoch < epochs; epoch++) - { - for (int i = 0; i < datasetSize; i += batchSize) - { - NDArray initialBatch = trainInitial[$"{i}:{i + batchSize}"]; - NDArray finalBatch = trainFinal[$"{i}:{i + batchSize}"]; - using (GradientTape tape = tf.GradientTape()) - { - Tensors predictedFinal = ForwardModel.Apply(initialBatch); - Tensors predictedInitial = ReverseModel.Apply(finalBatch); - Tensors reconstructedFinal = ForwardModel.Apply(predictedInitial); - - Tensor forwardLoss = keras.losses.BinaryCrossentropy().Call(finalBatch, predictedFinal); - Tensor reverseLoss = keras.losses.BinaryCrossentropy().Call(initialBatch, predictedInitial); - Tensor cycleLoss = keras.losses.BinaryCrossentropy().Call(finalBatch, reconstructedFinal); - Tensor totalLoss = forwardLoss + reverseLoss + cycleLoss; - Tensor[] gradients = tape.gradient(totalLoss, ForwardModel.TrainableVariables.Concat(ReverseModel.TrainableVariables)); - Optimizer.apply_gradients(zip(gradients, ForwardModel.TrainableVariables.Concat(ReverseModel.TrainableVariables))); - Console.WriteLine($"Epoch {epoch + 1}, Batch {i / batchSize + 1}, Loss: {totalLoss.numpy()}"); - } - - } - } - - - } - - public Board Predict(Board target) - { - float[] inputData = new float[Height * Width]; - foreach (var (x, y) in target.Lives) - inputData[y * Width + x] = 1f; - NDArray input = np.array(inputData).reshape(new Shape(1, Height, Width, 1)); - Tensors pred = ReverseModel.predict(input); - float[] predData = pred.ToArray(); - Board res = new Board(Width, Height); - for (int i = 0; i < predData.Length; i++) - { - int x = i % Width; - int y = i / Width; - if (predData[i] > 0.5f) - res.Lives.Add((x, y)); - } - return res; - } - - public static IEnumerable<(int, int)> Circle() - { - int centerX = 10; - int centerY = 10; - int radius = 7; - int x = 0; - int y = radius; - - int d = 1 - radius; - - IEnumerable<(int, int)> PlotCirclePoints(int cx, int cy, int px, int py) - { - yield return (cx + px, cy + py); - yield return (cx - px, cy + py); - yield return (cx + px, cy - py); - yield return (cx - px, cy - py); - yield return (cx + py, cy + px); - yield return (cx - py, cy + px); - yield return (cx + py, cy - px); - yield return (cx - py, cy - px); - } - - foreach (var point in PlotCirclePoints(centerX, centerY, x, y)) - yield return point; - - while (x < y) - { - x++; - if (d < 0) - d += 2 * x + 1; - else - { - y--; - d += 2 * (x - y) + 1; - } - - foreach (var point in PlotCirclePoints(centerX, centerY, x, y)) - yield return point; - } - } - public static void Run() - { - NeuralSolver solver = new NeuralSolver(20, 20, 10, false, false); - solver.Train(1000,8,20); - Board b = new Board(20, 20); - foreach ((int, int) cell in Circle()) - b.Toggle(cell); - b.Evaluate(10); - Board z = solver.Predict(b); - Console.WriteLine(z.ToString()); - - } -} diff --git a/src/NeuralSolver/NeuralSolver.cs b/src/NeuralSolver/NeuralSolver.cs new file mode 100644 index 0000000..cb51999 --- /dev/null +++ b/src/NeuralSolver/NeuralSolver.cs @@ -0,0 +1,188 @@ +using System.Text; +using System.Text.Json; +using InverseOfLife.NeuralSolver.Requests; +using InverseOfLife.NeuralSolver.Responses; + +namespace InverseOfLife.NeuralSolver; + +public class NeuralSolver : IDisposable +{ + + + + + + + + + private int Width { get; set; } + private int Height { get; set; } + private bool QuotientX { get; set; } + private bool QuotientY { get; set; } + private string NeuralBackend { get; set; } + private HttpClient HttpClient { get; set; } + + public NeuralSolver(int width, int height, bool quotientX = false, bool quotientY = false, + string neuralBackend = "http://localhost:8000") + { + Width = width; + Height = height; + QuotientX = quotientX; + QuotientY = quotientY; + NeuralBackend = neuralBackend; + HttpClient = new HttpClient(); + Initialize(); + } + + public void Initialize() + { + InitRequest initRequest = new InitRequest + { + Width = Width, + Height = Height, + QuotientX = QuotientX, + QuotientY = QuotientY + }; + string json = JsonSerializer.Serialize(initRequest); + Console.WriteLine(json); + StringContent content = new StringContent(json, Encoding.UTF8, "application/json"); + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/initialize", content).Result; + response.EnsureSuccessStatusCode(); + } + + public void Train(TrainRequest request) + { + string jsonContent = JsonSerializer.Serialize(request); + StringContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/train", content).Result; + response.EnsureSuccessStatusCode(); + string json = response.Content.ReadAsStringAsync().Result; + StatusResponse statusResponse = JsonSerializer.Deserialize(json); + while (true) + { + string status = BackendStatus(); + if (status == "ready" || status == "error") + return; + } + } + + public Board Predict(PredictRequest request) + { + string jsonContent = JsonSerializer.Serialize(request); + Console.WriteLine(jsonContent); + StringContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/predict", content).Result; + response.EnsureSuccessStatusCode(); + string json = response.Content.ReadAsStringAsync().Result; + PredictResponse res = JsonSerializer.Deserialize(json); + Board board = new Board(Width, Height, QuotientX, QuotientY); + foreach (List cell in res.Lives) + board.Lives.Add((cell[0], cell[1])); + return board; + } + + private string BackendStatus() + { + HttpResponseMessage response = HttpClient.GetAsync($"{NeuralBackend}/status").Result; + response.EnsureSuccessStatusCode(); + string json = response.Content.ReadAsStringAsync().Result; + return JsonSerializer.Deserialize( + json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ).Status; + } + + private void SaveModel() + { + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/save", null).Result; + response.EnsureSuccessStatusCode(); + string json = response.Content.ReadAsStringAsync().Result; + StatusResponse res = JsonSerializer.Deserialize(json); + if (res.Status != "ok") + throw new Exception(res.Status); + } + + private void LoadModel() + { + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/load", null).Result; + response.EnsureSuccessStatusCode(); + string json = response.Content.ReadAsStringAsync().Result; + StatusResponse res = JsonSerializer.Deserialize(json); + if (res.Status != "ok") + throw new Exception(res.Status); + } + + public static IEnumerable<(int, int)> Circle() + { + int centerX = 10; + int centerY = 10; + int radius = 7; + int x = 0; + int y = radius; + + int d = 1 - radius; + + IEnumerable<(int, int)> PlotCirclePoints(int cx, int cy, int px, int py) + { + yield return (cx + px, cy + py); + yield return (cx - px, cy + py); + yield return (cx + px, cy - py); + yield return (cx - px, cy - py); + yield return (cx + py, cy + px); + yield return (cx - py, cy + px); + yield return (cx + py, cy - px); + yield return (cx - py, cy - px); + } + + foreach (var point in PlotCirclePoints(centerX, centerY, x, y)) + yield return point; + + while (x < y) + { + x++; + if (d < 0) + d += 2 * x + 1; + else + { + y--; + d += 2 * (x - y) + 1; + } + + foreach (var point in PlotCirclePoints(centerX, centerY, x, y)) + yield return point; + } + } + + public static void Test() + { + using (NeuralSolver s = new NeuralSolver(20, 20, false, false)) + { + //s.Initialize(); + s.Train(new TrainRequest + { + ForwardDatasetSize = 2000, + ForwardBatchSize = 16, + ForwardEpochs = 15, + BackwardDatasetSize = 4000, + BackwardBatchSize = 16, + BackwardEpochs = 20, + }); + Board b = new Board(20, 20); + foreach ((int, int) p in Circle()) + b.Lives.Add(p); + b.Evaluate(); + Board h = s.Predict(new PredictRequest + { + Lives = b.Lives.Select(((int x, int y) cell) => new List{cell.x, cell.y}).ToList(), + Direction = "backward" + }); + Console.WriteLine(h.ToString()); + } + } + + public void Dispose() + { + HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/finish", null).Result; + } +} + diff --git a/src/NeuralSolver/Requests/InitRequest.cs b/src/NeuralSolver/Requests/InitRequest.cs new file mode 100644 index 0000000..00e0d62 --- /dev/null +++ b/src/NeuralSolver/Requests/InitRequest.cs @@ -0,0 +1,9 @@ +namespace InverseOfLife.NeuralSolver.Requests; + +public class InitRequest +{ + public int Width { get; set; } + public int Height { get; set; } + public bool QuotientX { get; set; } = false; + public bool QuotientY { get; set; } = false; +} \ No newline at end of file diff --git a/src/NeuralSolver/Requests/PredictRequest.cs b/src/NeuralSolver/Requests/PredictRequest.cs new file mode 100644 index 0000000..85e9b71 --- /dev/null +++ b/src/NeuralSolver/Requests/PredictRequest.cs @@ -0,0 +1,7 @@ +namespace InverseOfLife.NeuralSolver.Requests; + +public class PredictRequest +{ + public List> Lives { get; set; } + public string Direction { get; set; } = "forward"; +} \ No newline at end of file diff --git a/src/NeuralSolver/Requests/TrainRequest.cs b/src/NeuralSolver/Requests/TrainRequest.cs new file mode 100644 index 0000000..9a49a6b --- /dev/null +++ b/src/NeuralSolver/Requests/TrainRequest.cs @@ -0,0 +1,11 @@ +namespace InverseOfLife.NeuralSolver.Requests; + +public class TrainRequest +{ + public int ForwardDatasetSize { get; set; } = 1000; + public int ForwardBatchSize { get; set; } = 8; + public int ForwardEpochs { get; set; } = 10; + public int BackwardDatasetSize { get; set; } = 1000; + public int BackwardBatchSize { get; set; } = 8; + public int BackwardEpochs { get; set; } = 10; +} \ No newline at end of file diff --git a/src/NeuralSolver/Responses/PredictResponse.cs b/src/NeuralSolver/Responses/PredictResponse.cs new file mode 100644 index 0000000..26a528f --- /dev/null +++ b/src/NeuralSolver/Responses/PredictResponse.cs @@ -0,0 +1,6 @@ +namespace InverseOfLife.NeuralSolver.Responses; + +public class PredictResponse +{ + public List> Lives { get; set; } +} diff --git a/src/NeuralSolver/Responses/StatusResponse.cs b/src/NeuralSolver/Responses/StatusResponse.cs new file mode 100644 index 0000000..dcf0fd9 --- /dev/null +++ b/src/NeuralSolver/Responses/StatusResponse.cs @@ -0,0 +1,6 @@ +namespace InverseOfLife.NeuralSolver.Responses; + +public class StatusResponse +{ + public string Status { get; set; } +} \ No newline at end of file