折腾万岁!
是什么?怎么用?
前段时间突然有了在 C# 中调用 Keras 生成的模型的奇怪需求,本来想的是干脆直接调用 Python 脚本然后传个参进去,但是这样不是很好玩,Google 了一下发现竟然有人把 Python 的几个机器学习框架都移植到了 C# 下,而 Keras.NET 就是其中一个,这下就很有趣了。
Keras.NET 是 SciSharp(是不是很熟悉,Python 中的科学计算工具包叫做 SciPy,不过我不知道这两者是不是一家)移植的用于 .NET 的 Keras 框架,其最大的特点是尽量使 C# 中的语法与 Python 原版的相似。另外 SciSharp 还移植了 NumSharp 等一系列库。
基于上述特点,Python 代码只需做极少量的改动就能在 C# 中使用,如下所示( 代码摘自 Github 官方 repo)。
这是 Python 代码:
batch_size = 128 num_classes = 10 epochs = 12 # input image dimensions img_rows, img_cols = 28, 28 # the data, split between train and test sets (x_train, y_train), (x_test, y_test) = mnist.load_data() if K.image_data_format() == 'channels_first': x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols) x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols) input_shape = (1, img_rows, img_cols) else: x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1) x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1) input_shape = (img_rows, img_cols, 1) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 print('x_train shape:', x_train.shape) print(x_train.shape[0], 'train samples') print(x_test.shape[0], 'test samples') # convert class vectors to binary class matrices y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) model = Sequential() model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape)) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(128, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(num_classes, activation='softmax')) model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy']) model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1])
这是 C# 代码:
int batch_size = 128; int num_classes = 10; int epochs = 12; // input image dimensions int img_rows = 28, img_cols = 28; Shape input_shape = null; // the data, split between train and test sets var ((x_train, y_train), (x_test, y_test)) = MNIST.LoadData(); if(Backend.ImageDataFormat() == "channels_first") { x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols); x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols); input_shape = (1, img_rows, img_cols); } else { x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1); x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1); input_shape = (img_rows, img_cols, 1); } x_train = x_train.astype(np.float32); x_test = x_test.astype(np.float32); x_train /= 255; x_test /= 255; Console.WriteLine($"x_train shape: {x_train.shape}"); Console.WriteLine($"{x_train.shape[0]} train samples"); Console.WriteLine($"{x_test.shape[0]} test samples"); // convert class vectors to binary class matrices y_train = Util.ToCategorical(y_train, num_classes); y_test = Util.ToCategorical(y_test, num_classes); // Build CNN model var model = new Sequential(); model.Add(new Conv2D(32, kernel_size: (3, 3).ToTuple(), activation: "relu", input_shape: input_shape)); model.Add(new Conv2D(64, (3, 3).ToTuple(), activation: "relu")); model.Add(new MaxPooling2D(pool_size: (2, 2).ToTuple())); model.Add(new Dropout(0.25)); model.Add(new Flatten()); model.Add(new Dense(128, activation: "relu")); model.Add(new Dropout(0.5)); model.Add(new Dense(num_classes, activation: "softmax")); model.Compile(loss: "categorical_crossentropy", optimizer: new Adadelta(), metrics: new string[] { "accuracy" }); model.Fit(x_train, y_train, batch_size: batch_size, epochs: epochs, verbose: 1, validation_data: new NDarray[] { x_test, y_test }); var score = model.Evaluate(x_test, y_test, verbose: 0); Console.WriteLine($"Test loss: {score[0]}"); Console.WriteLine($"Test accuracy: {score[1]}");
一些小问题
slice
的替代
目前我用到的功能中和 Python 有点区别的是,尽管 NumSharp 通过字符串作为参数实现了 Python 中的 slice
功能,但是并不能像 Python 里一样使用 x = x[:, :, :, np.newaxis]
。不过好在就算是 Python,也有另一种方法完成同样的功能,就是 np.expand_dims
,对应在 C# 里则是 x = np.expand_dims(x, 3);
。
系统架构
需要注意的是,使用 Keras.NET 需要安装版本匹配的 Python,并且仅支持 64 位的 Windows,这也就意味着编译 .NET 程序时不能选择“首选 32 位”。
.NET Core 上的注意事项
最开始我的程序是用 WinForms (.NET Framework 4) 写的,跑起来没什么大问题,就是只能依附于 Visual Studio 的调试模式。后来在微软发布 .NET Core 3 并支持 WPF 之后,九月份迁移项目的时候就出现了问题,一直报一个奇怪的错误,给作者提了 issue 之后也没什么太大的进展。
这学期期末的时候又想起来这个事情,试了一下在 .NET Core 3 下的 Console App 里跑起来是没问题的。于是再次提了一个 issue。最终得知是因为一些 Python 模块会使用 sys.strerr
和 sys.stdout
来输出一些东西,而 WinForms 和 WPF 是没有控制台的——大概这也是之前不能脱离 Visual Studio 运行的一个问题吧。
解决办法是更新到最新的 Keras.NET 并使用 Keras.Keras.DisablePySysConsoleLog = true
来禁用这些输出。如果有获取这些输出的需求,可以使用以下代码:
string stdout = Keras.Keras.GetStdOut(); string stderr = Keras.Keras.GetStdError();
ML.NET+F#不好吗(x
ML.NET 回头去看看,F# 就算了(