NumSharp でニューラルネットワークに対する勾配

「ゼロから作る Deep Learning」を読んで C# でゼロから作ってみる試みは、NumSharp を使って最初からやり直して、ようやく4章終盤まできた。

今回は、ニューラルネットワークに対する勾配を実装してみた。

using NumSharp;

var net = new simpleNet();
Console.WriteLine(net.W.ToString()); // 重みパラメータ

var x = np.array(0.6, 0.9);
var p = net.predict(x);
Console.WriteLine(p.ToString());
Console.WriteLine(np.argmax(p)); // 最大値のインデックス

var t = np.array(0, 0, 1.0); // 正解ラベル
var loss = net.loss(x, t);
Console.WriteLine(loss.ToString());

Func<NDArray, NDArray> f = W => net.loss(x, t);
var dW = gradient.numerical_gradient(f, net.W);
Console.WriteLine(dW.ToString());

static class functions
{
    public static NDArray softmax(NDArray x)
    {
        var c = np.max(x);
        var exp_x = np.exp(x - c);
        var sum_exp_x = np.sum(exp_x, NPTypeCode.Double);
        var y = exp_x / sum_exp_x;
        return y;
    }

    public static NDArray cross_entropy_error(NDArray y, NDArray t)
    {
        var delta = 1e-7;
        return (-1) * np.sum(t * np.log(y + delta), NPTypeCode.Double);
    }
}

static class gradient
{
    static NDArray numerical_gradient_1d(Func<NDArray, NDArray> f, NDArray x)
    {
        var h = 1e-4; // 0.0001
        var grad = np.zeros_like(x); // x と同じ形状の配列を生成

        foreach (var idx in Enumerable.Range(0, x.size))
        {
            var tmp_val = (double)x[idx];

            // f(x + h) の計算
            x[idx] = tmp_val + h;
            var fxh1 = f(x);

            // f(x - h) の計算
            x[idx] = tmp_val - h;
            var fxh2 = f(x);

            grad[idx] = (fxh1 - fxh2) / (2 * h);
            x[idx] = tmp_val; // 値を元に戻す
        }

        return grad;
    }

    public static NDArray numerical_gradient(Func<NDArray, NDArray> f, NDArray X)
    {
        if (X.ndim == 1)
        {
            return numerical_gradient_1d(f, X);
        }
        else
        {
            var grad = np.zeros_like(X);

            for (var idx = 0; idx < X.ndim; idx++)
            {
                var x = X[idx];
                grad[idx] = numerical_gradient_1d(f, x);
            }

            return grad;
        }
    }
}

class simpleNet
{
    public NDArray W { get; }

    public simpleNet()
    {
        // ゼロから作る Deep Learning と同じ重みパラメーターを指定する。
        W = np.array(new double[,]
        {
            { 0.47355232, 0.9977393, 0.84668094 },
            { 0.85557411, 0.03563661, 0.69422093 }
        });
    }

    public NDArray predict(NDArray x)
    {
        // NumSharp が 1-D と 2-D の内積をサポートしていなかったので、
        // x を 2-D に変換して計算したあと、1-D に戻して回避。
        // 無理矢理。
        x = x.reshape_unsafe(2, 2);
        var y = np.dot(x, W);
        return y.reshape_unsafe(3);
    }

    public NDArray loss(NDArray x, NDArray t)
    {
        var z = predict(x);
        var y = functions.softmax(z);
        var loss = functions.cross_entropy_error(y, t);
        return loss;
    }
}

.NET 6 で実行。

NumSharp が 1-D と 2-D の内積をサポートしてないので、無理矢理 reshape して回避。一瞬、Numpy のバインディングライブラリの Numpy.NET に移行したくなった。今後も移行する可能性はゼロじゃないな。

何はともあれ、Math.NET Numerics を使っていたころに追いついた。