C#で自作のディープラーニングフレームワークを作る その2(活性化層の実装)

活性化層

今回は、活性化関数に相当する活性化層を実装します。ただし、Softmax関数に関しては少し特殊なので次回に。

活性化層のインターフェース

活性化層は、基本的に行列を入力して、入力行列と同じ大きさの行列が出力されます。前回と同様、出力先を呼び出し元で与える場合と、メソッド内で動的確保する場合の2種類のメソッドを用意しています。

using MathNet.Numerics.LinearAlgebra.Single;

namespace NeuralNET.Layers.Activation
{
    /// <summary>
    /// 活性化層(活性化関数)が実装するインターフェース
    /// </summary>
    public interface IActivationLayer : ILayer
    {
        public DenseMatrix Forward(DenseMatrix x, DenseMatrix y);
        public DenseMatrix Forward(DenseMatrix x);

        public DenseMatrix Backward(DenseMatrix dOutput, DenseMatrix res);
        public DenseMatrix Backward(DenseMatrix dOutput);
    }
}

シグモイド層

シグモイド関数(標準シグモイド関数) f(x) = \frac{1}{1 - e^{-x}}を計算する層です。この関数は、よく中間層の活性化関数や2値分類問題において出力層の活性化関数に用いられます。微分するとf'(x) = f(x)(1 - f(x))となり、元の関数が導関数に登場する形になります。それゆえ、以下のSigmoidLayerクラスでは、順伝播時に出力を保存します。

(2024/04/07 追記) SigmoidLayer、TanhLayer、ReLULayerそれぞれについて、Backwardメソッドにバグがあったので修正しました。

using MathNet.Numerics.LinearAlgebra.Single;

namespace NeuralNET.Layers.Activation
{
    /// <summary>
    /// 標準シグモイド関数
    /// </summary>
    public class SigmoidLayer : IActivationLayer
    {
        DenseMatrix? output;
        readonly bool SAVE_OUTPUT_REF;

        public SigmoidLayer() : this(false) { }

        public SigmoidLayer(bool saveOutputRef) => this.SAVE_OUTPUT_REF = saveOutputRef;

        public DenseMatrix Forward(DenseMatrix x, DenseMatrix y)
        {
            x.PointwiseSigmoid(y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Forward(DenseMatrix x)
        {
            var y = DenseMatrix.Create(x.RowCount, x.ColumnCount, 0.0f);
            Forward(x, y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Backward(DenseMatrix dOutput, DenseMatrix res)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            this.output.Negate(res);
            res.Add(1.0f, res);
            res.PointwiseMultiply(this.output, res);
            res.PointwiseMultiply(dOutput, res);
            return res;
        }

        public DenseMatrix Backward(DenseMatrix dOutput)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            var res = DenseMatrix.Create(this.output.RowCount, this.output.ColumnCount, 0.0f);
            return Backward(dOutput, res);
        }

        void SaveOutput(DenseMatrix output)
        {
            if (this.SAVE_OUTPUT_REF)
            {
                this.output = output;
                return;
            }

            this.output = output.CopyToOrClone(this.output);
        }
    }
}

DenseMatrix.PointwiseSigmoidメソッドは自作の拡張メソッドであり、LinearAlgebraExtensionsクラス内で以下のように実装しています。
(2024/04/07 追記) シグモイド関数のバグを修正しました.

public static void PointwiseSigmoid(this DenseMatrix x, DenseMatrix y)
{
       x.Negate(y);
       y.PointwiseExp(y);
       y.Add(1.0f, y);
       y.Map(x => 1.0f / x, y);
}

複数のメソッドを組み合わせて表現しているので、ぱっと見、コードからは数式が浮かびにくいです。まあ、しょうがない。

tanh

tanh関数(双曲正接関数)を計算する層です。値域は異なりますが、tanh関数はシグモイド関数の一種です。そのため、シグモイド関数と同様に導関数に自分自身が登場します。

\{\tanh(x)\}' = \frac{1}{\cosh(x)} = 1 - \tanh^2(x)

using MathNet.Numerics.LinearAlgebra.Single;

namespace NeuralNET.Layers.Activation
{
    /// <summary>
    /// tanh関数
    /// </summary>
    public class TanhLayer : IActivationLayer
    {
        DenseMatrix? output;
        readonly bool SAVE_OUTPUT_REF;

        public TanhLayer() : this(false) { }

        public TanhLayer(bool saveOutputRef) => this.SAVE_OUTPUT_REF = saveOutputRef;

        public DenseMatrix Forward(DenseMatrix x, DenseMatrix y)
        {
            x.PointwiseTanh(y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Forward(DenseMatrix x)
        {
            var y = DenseMatrix.Create(x.RowCount, x.ColumnCount, 0.0f);
            Forward(x, y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Backward(DenseMatrix dOutput, DenseMatrix res)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            this.output.PointwiseMultiply(this.output, res);
            res.Negate(res);
            res.Add(1.0f, res);
            res.PointwiseMultiply(dOutput, res);
            return res;
        }

        public DenseMatrix Backward(DenseMatrix dOutput)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            var res = DenseMatrix.Create(this.output.RowCount, this.output.ColumnCount, 0.0f);
            return Backward(dOutput, res);
        }

        void SaveOutput(DenseMatrix output)
        {
            if (this.SAVE_OUTPUT_REF)
            {
                this.output = output;
                return;
            }

            this.output = output.CopyToOrClone(this.output);
        }
    }
}

Math.NETには、行列の要素ごとにtanh関数を適用するメソッドが用意されているので、容易に実装できます。

ReLU層

ReLU関数を計算する層です。ReLU関数は、入力が0以下なら0を、0以上なら入力された値をそのまま出力する単純な関数です。中間層の活性化関数に非常によく用いられます。ReLUの導関数はReLU関数の出力が0以下ならば0を、そうでなければ1をとる単純な関数です*1シグモイド関数導関数と比べると0と1しか出力しないので、勾配消失が起きづらいというメリットがあります。

using MathNet.Numerics.LinearAlgebra.Single;

namespace NeuralNET.Layers.Activation
{
    /// <summary>
    /// ReLU関数
    /// </summary>
    public class ReLULayer : IActivationLayer
    {
        DenseMatrix? output;
        readonly bool SAVE_OUTPUT_REF;

        public ReLULayer() : this(false) { }

        public ReLULayer(bool saveOutputRef) => this.SAVE_OUTPUT_REF = saveOutputRef;

        public DenseMatrix Forward(DenseMatrix x, DenseMatrix y)
        {
            x.PointwiseMaximum(0.0f, y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Forward(DenseMatrix x)
        {
            var y = DenseMatrix.Create(x.RowCount, x.ColumnCount, 0.0f);
            Forward(x, y);
            SaveOutput(y);
            return y;
        }

        public DenseMatrix Backward(DenseMatrix dOutput, DenseMatrix res)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            this.output.PointwiseSign(res);
            res.PointwiseMultiply(dOutput, res);
            return res;
        }

        public DenseMatrix Backward(DenseMatrix dOutput)
        {
            if (this.output is null)
                throw new InvalidOperationException("Backward method must be called after forward.");

            var res = DenseMatrix.Create(this.output.RowCount, this.output.ColumnCount, 0.0f);
            return Backward(dOutput, res);
        }

        void SaveOutput(DenseMatrix output)
        {
            if (this.SAVE_OUTPUT_REF)
            {
                this.output = output;
                return;
            }

            this.output = output.CopyToOrClone(this.output);
        }
    }
}

すでにMath.NET側で用意されている、DenseMatrix.PointwiseMaximumメソッドとDenseMatrix.PointwiseSignメソッドを利用して容易に実装できます。

次回

次回は分類問題でよく用いられるSoftmax関数を実装します。

今回のコミットは以下です。
まだテストをしていないので、後々バグが見つかる可能性もあります。

github.com

*1:厳密にはReLU関数は、x=0微分不可能なのですが、便宜上x=0の時は、勾配が0になるとしています。