improve: use python service api to provide tensorflow instead of using tensorflow.net

This commit is contained in:
h z
2025-01-27 10:08:31 +00:00
parent 98606b4938
commit ed9bfe9869
7 changed files with 227 additions and 198 deletions

View File

@@ -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<float>();
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());
}
}

View File

@@ -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<StatusResponse>(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<PredictResponse>(json);
Board board = new Board(Width, Height, QuotientX, QuotientY);
foreach (List<int> 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<StatusResponse>(
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<StatusResponse>(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<StatusResponse>(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<int>{cell.x, cell.y}).ToList(),
Direction = "backward"
});
Console.WriteLine(h.ToString());
}
}
public void Dispose()
{
HttpResponseMessage response = HttpClient.PostAsync($"{NeuralBackend}/finish", null).Result;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
namespace InverseOfLife.NeuralSolver.Requests;
public class PredictRequest
{
public List<List<int>> Lives { get; set; }
public string Direction { get; set; } = "forward";
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
namespace InverseOfLife.NeuralSolver.Responses;
public class PredictResponse
{
public List<List<int>> Lives { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace InverseOfLife.NeuralSolver.Responses;
public class StatusResponse
{
public string Status { get; set; }
}