生之为萌,乐享创造

在 C# 中使用 Keras

折腾万岁!

是什么?怎么用?

前段时间突然有了在 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.strerrsys.stdout来输出一些东西,而 WinForms 和 WPF 是没有控制台的——大概这也是之前不能脱离 Visual Studio 运行的一个问题吧。

解决办法是更新到最新的 Keras.NET 并使用Keras.Keras.DisablePySysConsoleLog = true来禁用这些输出。如果有获取这些输出的需求,可以使用以下代码:

string stdout = Keras.Keras.GetStdOut();
string stderr = Keras.Keras.GetStdError();

发表评论

电子邮件地址不会被公开。 必填项已用*标注