FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ17/ステップ18)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作るDeep Learning ❸ ―フレームワーク編

  • 作者:斎藤 康毅
  • 発売日: 2020/04/20
  • メディア: 単行本(ソフトカバー)

ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ17とステップ18。

  • ステップ17:メモリの使用量を削減。

循環参照をしている場合にGCで発見して削除するのには時間がかかる場合がある。従ってoutputの接続関係についてはWearkrefを導入して参照カウントを増やさない工夫をし、循環参照になることを防いでGCが即座に適用されるように工夫する。

RubyでもWeakrefってあるのかしらと思って調べてみたらあった。

require 'weakref'
...
class Function
  def call(*inputs)
    xs = inputs.map{|x| x.data}
...
    @outputs = outputs.map{|output| WeakRef.new(output)}
    return outputs.size > 1 ? outputs : outputs[0]
  end

これでメモリ使用量を減らせるかなと思ったらあまり変わらない。現状ではあまり大差がないのだろうか?

  • WeakRefを有効にした場合
Process: 15054: RSS = 11.888 MB, VSZ = 26.144000000000002 MB
Process: 15054: RSS = 12.232000000000001 MB, VSZ = 26.468 MB
Process: 15054: RSS = 12.552 MB, VSZ = 26.788 MB
Process: 15054: RSS = 12.864 MB, VSZ = 27.112000000000002 MB
Process: 15054: RSS = 13.184000000000001 MB, VSZ = 27.432000000000002 MB
Process: 15054: RSS = 13.496 MB, VSZ = 27.756 MB
Process: 15054: RSS = 13.816 MB, VSZ = 28.076 MB
Process: 15054: RSS = 14.128 MB, VSZ = 28.400000000000002 MB
Process: 15054: RSS = 14.448 MB, VSZ = 28.72 MB
Process: 15054: RSS = 14.76 MB, VSZ = 29.044 MB
  • WeakRefを無効にした場合
Process: 15085: RSS = 11.896 MB, VSZ = 26.144000000000002 MB
Process: 15085: RSS = 12.24 MB, VSZ = 26.464000000000002 MB
Process: 15085: RSS = 12.552 MB, VSZ = 26.788 MB
Process: 15085: RSS = 12.872 MB, VSZ = 27.108 MB
Process: 15085: RSS = 13.184000000000001 MB, VSZ = 27.432000000000002 MB
Process: 15085: RSS = 13.504 MB, VSZ = 27.752 MB
Process: 15085: RSS = 13.816 MB, VSZ = 28.076 MB
Process: 15085: RSS = 14.136000000000001 MB, VSZ = 28.396 MB
Process: 15085: RSS = 14.448 MB, VSZ = 28.72 MB
Process: 15085: RSS = 14.768 MB, VSZ = 29.04 MB
  • ステップ18:メモリの使用量を削減するためのモード。

まずは不要な微分を保持しないためにretain_gradを追加してこの変数がfalseの時はgradの値を保持しないようにする。

class Variable
...
  def backward(retain_grad=false)
    if @grad == nil then
      @grad = fill_one(@data.clone)
...
    while not funcs.empty? do
      f = funcs.pop
...
      }
      if not retain_grad then
        f.outputs.map{|y| y.grad = nil }
      end
    end

こうすると中間のx.gradが保持されなくなる。nilのままだ。


2.0
1.0

続いて逆伝搬の無効モードを付け加える。逆伝搬の無効モードでは、backward()を実行されない。順方向だけのニューラルネットワークテスト用に使用する。Pythonではconfig::enable_backpropというクラス内メンバを使用していたが、Rubyで同じことをする方法が分からない。仕方が無いので大域変数$enable_backpropを定義してそれを使用した。

class Function
  def call(*inputs)
    xs = inputs.map{|x| x.data}
    ys = forward(*xs)
    if ys.is_a?(Array) then
      ys = [ys]
    end
    outputs = ys.map{|y| Variable.new(y) }

    if $enable_backprop then
      @generation = (inputs.map{|x| x.generation}).max
      outputs.each {|output| output.set_creator(self) }
    end

    @inputs = inputs
    @outputs = outputs.map{|output| WeakRef.new(output)}
    return outputs.size > 1 ? outputs : outputs[0]
  end
...

このテスト実行中に間違いを発見した。backward実行時に、backwardの初期値が存在しない場合は1.0で埋められた行列を作ってそれを入力値としていたのだが、その作り方が間違っている。

  def backward()
    if @grad == nil then
      @grad = @data.clone.fill(1.0)
    end
    funcs = []

これでは@gradは1次元の配列にしかならない。以下のように変更した。

def fill_one(a)
  if a.is_a?(Array)
    a.map{|i| fill_one(i) }
  else
    a = 1.0
  end
end

class Variable
...
  def backward(retain_grad=false)
    if @grad == nil then
      @grad = fill_one(@data.clone)
    end

2つのモードを用いてテストを行う。

begin
  $enable_backprop = false
  x = Variable.new(100.times.map{100.times.map{100.times.map{1.0}}})
  y = square(square(square(x)))
  y.backward()
end

begin
  $enable_backprop = true
  x = Variable.new(100.times.map{100.times.map{100.times.map{1.0}}})
  y = square(square(square(x)))
  y.backward()
end

最後のwithブロックを用いる切替については省略。

LLVMの新しい中間言語表現 MLIRを試す(2. MLIRに関するコード生成を試す)

MLIRについて基礎を学んだところで、実際に動かしてみたい。以下のページを読みながら少しチュートリアルを触ってみよう。

mlir.llvm.org

mlir.llvm.org


MLIRとのインタフェース

先ほどのtranspose()がどのようにMLIRに変換されたのかを以下に示す。

%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64> loc("example/file/path":12:1)
  • t_tnsortransposeの結果に付けられる名前。SSA値として表現される。
  • "toy.transpose":操作の名前
  • (%tensor):引数のリスト。
  • {inplace = true}:操作に付けられる属性。ここではinplaceというBool型のTrue値を持つ値を定義している。
  • (tensor<2x3xf64>) -> tensor<3x2xf64>:関数形式での型の変換形式を示している。引数の型、およびその戻り値の型を示している。
  • loc("example/file/path":12:1):この操作が発生したソースコードの場所を示している。

MLIRとのインタフェースのためのToy方言

Toy言語に対する方言を定義するために、C++ToyDialectを定義する。

/// This is the definition of the Toy dialect. A dialect inherits from
/// mlir::Dialect and registers custom attributes, operations, and types (in its
/// constructor). It can also override virtual methods to change some general
/// behavior, which will be demonstrated in later chapters of the tutorial.
class ToyDialect : public mlir::Dialect {
 public:
  explicit ToyDialect(mlir::MLIRContext *ctx);

  /// Provide a utility accessor to the dialect namespace. This is used by
  /// several utilities.
  static llvm::StringRef getDialectNamespace() { return "toy"; }
};

これをDialectのためのグローバルレジスタに登録する。

  mlir::registerDialect<ToyDialect>();

Toy Operationの定義

新しいtoy.constantオペレーションを定義する。

 %4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64>

新しいクラスとしてConstantOpを作成する。ConstantOpにいくつかのメソッドを定義してやらなければならない。

class ConstantOp : public mlir::Op<ConstantOp,
                     /// ConstantOpは引数を何も受け取らない。
                     mlir::OpTrait::ZeroOperands,
                     /// ConstantOpは1つの戻り値を返す。
                     mlir::OpTrait::OneResult> {

 public:
  /// コンストラクタはベースのクラスから継承する。
  using Op::Op;

  /// この操作に対するユニークな操作名を定義する。MLIRはこの名前を登録してシステム中で
  /// ユニークな名前として使用する。
  static llvm::StringRef getOperationName() { return "toy.constant"; }

  /// この属性から定数を読み取って値を返す。
  mlir::DenseElementsAttr getValue();

  /// 定義したトレイトを超えた追加の検証を提供することができる。ここでは特定の定数に対する
  /// 普遍量が守られていることを確認する。例えば、結果の値はTensorTypeでなければならない。
  LogicalResult verify();
                         
  /// `value`の属性を持つ戻り値を生成するための定数を構築する。
  static void build(mlir::OpBuilder &builder, mlir::OperationState &state,
                    mlir::Type result, mlir::DenseElementsAttr value);
  /// Build a constant and reuse the type from the given 'value'.
  /// valueの属性を再利用して定数を生成する。
  static void build(mlir::OpBuilder &builder, mlir::OperationState &state,
                    mlir::DenseElementsAttr value);
  /// Build a constant by broadcasting the given 'value'.
  /// `value`をブロードキャストすることで定数を生成する。
  static void build(mlir::OpBuilder &builder, mlir::OperationState &state,
                    double value);
};

オペレーションとオペレーション:MLIRオペレーションの使用

新しいオペレーションを定義するためには2つの主要なクラスについて知る必要がある。

  • Operation :すべてのオペレーションを包括するために使用する。
  • Op:特定の型に対する操作を実装する。

Operation Definition Specification(ODS)フレームワークを使用してOperationを定義する

mlir::Opを使用せずにDSLを使ってTableGen経由でオペレーションを定義することもできる。おそらくこちらの方が推奨されている。

// 'toy' の方言をODSフレームワークに提供し、操作を定義できるようにする。
def Toy_Dialect : Dialect {
  // The namespace of our dialect, this corresponds 1-1 with the string we
  // provided in `ToyDialect::getDialectNamespace`.
  let name = "toy";

  // The C++ namespace that the dialect class definition resides in.
  let cppNamespace = "toy";
}

Opクラスからの継承によってオペレーションを定義する。

// Base class for toy dialect operations. This operation inherits from the base
// `Op` class in OpBase.td, and provides:
//   * The parent dialect of the operation.
//   * The mnemonic for the operation, or the name without the dialect prefix.
//   * A list of traits for the operation.
class Toy_Op<string mnemonic, list<OpTrait> traits = []> :
    Op<Toy_Dialect, mnemonic, traits>;

C++のコードを参照するためには以下のように入力すればよいらしい。

${build_root}/bin/mlir-tblgen -gen-op-defs ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/

Ops.tdConstantOpの定義は以下のようになっていた。

// We define a toy operation by inheriting from our base 'Toy_Op' class above.
// Here we provide the mnemonic and a list of traits for the operation. The
// constant operation is marked as 'NoSideEffect' as it is a pure operation
// and may be removed if dead.
def ConstantOp : Toy_Op<"constant", [NoSideEffect]> {
  // Provide a summary and description for this operation. This can be used to
  // auto-generate documentation of the operations within our dialect.
  let summary = "constant";
  let description = [{
    Constant operation turns a literal into an SSA value. The data is attached
    to the operation as an attribute. For example:

    ```mlir
      %0 = "toy.constant"()
         { value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> }
        : () -> tensor<2x3xf64>
    ```
  }];

  // The constant operation takes an attribute as the only input.
  let arguments = (ins F64ElementsAttr:$value);

  // The constant operation returns a single value of TensorType.
  let results = (outs F64Tensor);

  // Add custom build methods for the constant operation. These method populates
  // the `state` that MLIR uses to create operations, i.e. these are used when
  // using `builder.create<ConstantOp>(...)`.
  let builders = [
    // Build a constant with a given constant tensor value.
    OpBuilder<"Builder *builder, OperationState &state, "
              "DenseElementsAttr value", [{
      build(builder, state, value.getType(), value);
    }]>,

    // Build a constant with a given constant floating-point value.
    OpBuilder<"Builder *builder, OperationState &state, double value">
  ];

  // Invoke a static verify method to verify this constant operation.
  let verifier = [{ return ::verify(*this); }];
}

-gen-op-defsを用いて生成したC++の結果はこちら。

//===----------------------------------------------------------------------===//
// toy::ConstantOp definitions
//===----------------------------------------------------------------------===//

ConstantOpOperandAdaptor::ConstantOpOperandAdaptor(ArrayRef<Value> values) {
  tblgen_operands = values;
}

ArrayRef<Value> ConstantOpOperandAdaptor::getODSOperands(unsigned index) {
  return {std::next(tblgen_operands.begin(), index), std::next(tblgen_operands.begin(), index + 1)};
}

StringRef ConstantOp::getOperationName() {
  return "toy.constant";
}

Operation::operand_range ConstantOp::getODSOperands(unsigned index) {
  return {std::next(getOperation()->operand_begin(), index), std::next(getOperation()->operand_begin(), index + 1)};
}

Operation::result_range ConstantOp::getODSResults(unsigned index) {
  return {std::next(getOperation()->result_begin(), index), std::next(getOperation()->result_begin(), index + 1)};
}

DenseElementsAttr ConstantOp::valueAttr() {
  return this->getAttr("value").cast<DenseElementsAttr>();
}

DenseElementsAttr ConstantOp::value() {
  auto attr = valueAttr();
  return attr;
}

void ConstantOp::build(Builder *builder, OperationState &state, DenseElementsAttr value) {
      build(builder, state, value.getType(), value);

}
...

-get-op-declsを用いて生成したC++の結果はこちら。

//===----------------------------------------------------------------------===//
// toy::ConstantOp declarations
//===----------------------------------------------------------------------===//

class ConstantOpOperandAdaptor {
public:
  ConstantOpOperandAdaptor(ArrayRef<Value> values);
  ArrayRef<Value> getODSOperands(unsigned index);

private:
  ArrayRef<Value> tblgen_operands;
};
class ConstantOp : public Op<ConstantOp, OpTrait::OneResult, OpTrait::HasNoSideEffect, OpTrait::ZeroOperands> {
public:
  using Op::Op;
  using OperandAdaptor = ConstantOpOperandAdaptor;
  static StringRef getOperationName();
  Operation::operand_range getODSOperands(unsigned index);
  Operation::result_range getODSResults(unsigned index);
  DenseElementsAttr valueAttr();
  DenseElementsAttr value();
  static void build(Builder *builder, OperationState &state, DenseElementsAttr value);
  static void build(Builder *builder, OperationState &state, double value);
  static void build(Builder *tblgen_builder, OperationState &tblgen_state, Type resultType0, DenseElementsAttr value);
  static void build(Builder *tblgen_builder, OperationState &tblgen_state, ArrayRef<Type> resultTypes, DenseElementsAttr value);
  static void build(Builder *, OperationState &tblgen_state, ArrayRef<Type> resultTypes, ValueRange operands, ArrayRef<NamedAttribute> attributes);
  LogicalResult verify();
};

LLVMの新しい中間言語表現 MLIRを試す(1. Getting Started)

MLIRについて基礎を学んだところで、実際に動かしてみたい。以下のページを読みながら少しチュートリアルを触ってみよう。

https://mlir.llvm.org/getting_started/

ビルドのためのリポジトリLLVMのものと同じで良いらしい。

git clone https://github.com/llvm/llvm-project.git
mkdir llvm-project/build
cd llvm-project/build
cmake -G Ninja ../llvm \
   -DLLVM_ENABLE_PROJECTS=mlir \
   -DLLVM_BUILD_EXAMPLES=ON \
   -DLLVM_TARGETS_TO_BUILD="X86;NVPTX;AMDGPU" \
   -DCMAKE_BUILD_TYPE=Release \
   -DLLVM_ENABLE_ASSERTIONS=ON \
#  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_ENABLE_LLD=ON

cmake --build . --target check-mlir

ターゲットとしてはX86, NVPTX, AMDGPUとしており、 Releaseビルドで実行している。Releaseなのでさして時間はかからない。放っておけば終了する。

ビルドディレクトリに生成されたのは以下のファイルだ。toyのファイルがいくつか含まれているな。これを見ながら勉強しろ、ということなんだろうか。

$ ls -1 build/bin
FileCheck
count
llvm-lit
llvm-tblgen
mlir-cpu-runner
mlir-edsc-builder-api-test
mlir-opt
mlir-sdbm-api-test
mlir-tblgen
mlir-translate
not
toyc-ch1
toyc-ch2
toyc-ch3
toyc-ch4
toyc-ch5
toyc-ch6
toyc-ch7

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ15/ステップ16)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作るDeep Learning ❸ ―フレームワーク編

  • 作者:斎藤 康毅
  • 発売日: 2020/04/20
  • メディア: 単行本(ソフトカバー)

ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ15とステップ16。

  • ステップ15とステップ16は同じ内容で理論編と実践編となっている。複雑な計算グラフを取り扱うために、ネットワークのツリーをDFSで追うのではなくBFSで追いかけるように変更する。

generationメンバ変数を追加して、generationの値の大きい順にbackwardを適用することでBFSでネットワークを逆方向に辿れるようにする。

class Variable
...
  def backward()
    if @grad == nil then
      @grad = @data.clone.fill(1.0)
    end
    funcs = []
    seen_set = Set.new

    def add_func(f, funcs, seen_set)
      if not seen_set.include?(f) then
        funcs.push(f)
        seen_set.add(f)
        funcs.sort!{|a| a.generation}
      end
    end

    add_func(@creator, funcs, seen_set)
    while funcs != [] do
      f = funcs.pop
      gys = f.outputs.map{|x| x.grad}
      gxs = f.backward(*gys)
      if not gxs.is_a?(Array) then
        gxs = [gxs]
      end
      f.inputs.zip(gxs).each{|x, gx|
        if x.grad === nil then
          x.grad = gx
        else
          tmp = (x.grad + gx)
          x.grad = [tmp.is_a?(Array) ? tmp.sum : tmp]
        end
        if x.creator != nil then
          add_func(x.creator, funcs, seen_set)
        end
      }
    end
  end

Rubyset型を使用して関数の登場順番を管理している。seen_set型を使ってすでに同じノードを処理したかどうかを見ている。generationの順番によって適用順序を切り替えているのは、funcs.sort!{|a| a.generation}によって制御している。

x = Variable.new([2.0])
a = square(x)
y = add(square(a), square(a))
y.backward()

puts(y.data)
puts(x.grad)
32.0
64.0

問題無く動作したようだ。

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ13/ステップ14)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作るDeep Learning ❸ ―フレームワーク編

  • 作者:斎藤 康毅
  • 発売日: 2020/04/20
  • メディア: 単行本(ソフトカバー)

ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ13とステップ14。

  • ステップ13:逆伝搬における可変長引数のサポート

前回に続いて可変長引数時の逆伝搬をサポートする。backward()に以下の修正を加える。

  • step13.rb
class Variable
...
  def backward()
    if @grad == nil then
      @grad = @data.clone.fill(1.0)
    end
    funcs = [@creator]
    while funcs != [] do
      f = funcs.pop
      gys = f.outputs.map{|x| x.grad}
      gxs = f.backward(*gys)
      if not gxs.is_a?(Array) then
        gxs = [gxs]
      end
      f.inputs.zip(gxs).each{|x, gx|
        x.grad = gx
        if x.creator != nil then
          funcs.push(x.creator)
        end
      }
    end
  end

要点としてはf.outputsf.inputsが可変長引数を受け取ることができるように拡張されている。f.outputsに対してx.gradの要素を取り出すためにmapを使ったりして、backward(gys)に対して任意の長さの配列が受け付けられるようになっている。

これに伴ってSquareクラスの実装を変更した。

  • step13.rb
class Square < Function
...
  def backward(gy)
    x = @inputs[0].data
    gx = x.zip(gy).map{|i0, i1| i0 * i1 * 2.0}
    return gx
  end

inputsが配列へと変更されたため@inputs[0]として先頭の値を取り出すように変更されている。

これまでと同じようにテストを作って実行してみる。

def add(x0, x1)
  return Add.new().call(x0, x1)
end

x = Variable.new([2.0])
y = Variable.new([3.0])
z = add(square(x), square(y))
z.backward()
puts(z.data)
puts(x.grad)
puts(y.grad)

 z = x^{2} + y^{2}微分を行うテストだ。x y について微分を行う。

13.0
4.0
6.0

上手くできているようだ。

  • ステップ14:同じ変数を繰り返し使う場合の考慮

現状の実装では同じ変数を売り返して使う場合の考慮がなされていない。複数の場所で同じ変数を使用した場合には、backwardにおいて微分した値を累積する必要がある。

  • step14.rb
class Variable
...
  def backward()
    if @grad == nil then
      @grad = @data.clone.fill(1.0)
    end
    funcs = [@creator]
    while funcs != [] do
...
      f.inputs.zip(gxs).each{|x, gx|
        if x.grad === nil then
          x.grad = gx
        else
          x.grad = [(x.grad + gx).sum]
        end
        if x.creator != nil then
          funcs.push(x.creator)
        end
      }
...

テストを行う。

begin
  x = Variable.new([3.0])
  y = add(x, x)
  y.backward()
  puts(x.grad)
end

begin
  x = Variable.new([3.0])
  y = add(add(x, x),x)
  y.backward()
  puts(x.grad)
end
2.0
3.0

それともう一つ、微分の値をリセットするためのメソッドを定義しておいた。

class Variable
...
  def cleargrad()
    @grad = nil
  end
...
  x = Variable.new([3.0])
  y = add(x, x)
  y.backward()
  puts(x.grad)

  x = Variable.new([3.0])
  y = add(add(x, x),x)
  y.backward()
  puts(x.grad)
2.0
3.0

こちら問題なさそう。

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ11/ステップ12)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作るDeep Learning ❸ ―フレームワーク編

  • 作者:斎藤 康毅
  • 発売日: 2020/04/20
  • メディア: 単行本(ソフトカバー)

ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ11とステップ12。

  • ステップ11:可変長引数のサポート

まずはcall()に可変長引数をサポートする。複数の引数を取ることができるようにするために、受け取った配列を受け取って配列を組みなおす。入力inputsに対してmapを適用してデータを取り出す。これに対してforward()を適用し、それをもう一度Variableをラップし直す。

 class Function
   def call(inputs)
     xs = inputs.map{|x| x.data}
     ys = forward(xs)
     outputs = ys.map{|y| Variable.new(y) }

     outputs.each {|output|
       output.set_creator(self)
     }
     @inputs = inputs
     @outputs = outputs
     return outputs
   end

この変更に基づいてAdd()関数を作ってみる。

 class Add < Function
   def forward(xs)
     y = xs[0][0] + xs[1][0]
     return [y]
   end
 end

forward()は複数長の配列を受け取り、その2つの要素を加算して返す。テストコードは以下のようになる。

 xs = [Variable.new([2.0]), Variable.new([3.0])]
 f = Add.new()
 ys = f.call(xs)
 y = ys[0]
 puts y.data
5.0

複数サイズの配列を受け取って、計算することができた。

  • ステップ11:可変長引数のサポート

Rubyでも可変長をサポートすることができる。Pythonと同じ表現形式かな?

 class Function
   def call(*inputs)
     xs = inputs.map{|x| x.data}
     ys = forward(xs)
     outputs = ys.map{|y| Variable.new(y) }

     outputs.each {|output|
       output.set_creator(self)
     }
     @inputs = inputs
     @outputs = outputs
     return outputs.size > 1 ? outputs : outputs[0]
   end
 class Add < Function
   def forward(xs)
     y = xs[0][0] + xs[1][0]
     return [y]
   end
 end

 x0 = Variable.new([2.0])
 x1 = Variable.new([3.0])
 f = Add.new()
 y = f.call(x0, x1)
 puts y.data

Add()クラスでは複数要素の可変長引数を1つの配列にまとめ、forward()に渡すことで計算できる。

5.0

続いてAdd()クラスの引数の受け取り方の改善を行う。

 class Add < Function
   def forward(x0, x1)
     y = x0[0] + x1[0]
     return y
   end
 end

これはあまり前回のコードと違いが無いのだが、とりあえず可変長で受け取ることができるだけ進歩している。call()も可変長引数、forward()も可変長で受け取ることができるので、以下のような表現が可能となる。

 def add(x0, x1)
   return Add.new().call(x0, x1)
 end

 x0 = Variable.new([2.0])
 x1 = Variable.new([3.0])
 y = add(x0, x1)
 puts y.data
5.0

できた。

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ9/ステップ10)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作るDeep Learning ❸ ―フレームワーク編

  • 作者:斎藤 康毅
  • 発売日: 2020/04/20
  • メディア: 単行本(ソフトカバー)

ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ9とステップ10。

  • ステップ9:関数をより便利に使う

これまでSquareクラスやExpクラスは明示的に宣言して使用する必要があった。これは面倒なのでWapperを作って簡単に関数を接続できるようにする。

  • test09.rb
def square(x)
  return Square.new().call(x)
end

def exp(x)
  return Exp.new().call(x)
end

これによりニューラルネットワークを数珠つなぎのように表現できるようになった。

  x = Variable.new([0.5])
  y = square(exp(square(x)))    # 数珠つなぎのように関数の連結を表現する。

  y.grad = [1.0]
  y.backward()
  puts(x.grad)

さらに、backword()の前にy.gradの初期値1.0を設定しなくても良いようにする。

class Variable
    ...
  def backward()
    if @grad == nil then
      @grad = @data.clone.fill(1.0)
    end

それにしてもNumPyには便利な関数がたくさんあるようで、np.ones_like()などという超絶便利な関数はRubyで存在しなかったので、@dataと同じ形の変数をcloneし、fillで全部1.0を埋め込んでしまうという無理やりな作戦を使った。とりあえずこれで代用できた。

これに加えて、いくつか型の制限を設けて誤ったコードの実行を防ぐようにする。

class Variable
  def initialize(data)
    if data != nil then
      if not data.is_a?(Array) then
        raise TypeError, data.class.to_s + " is not supported."
      end
    end
...

Array型以外のデータを変数として使用しようとするとエラーを出すようにした。

これにより、誤ってArray以外のデータを入力するとエラーが出るようにする。

  x = Variable.new([0.5])
  y = square(exp(square(x)))
  y.backward()
  puts(x.grad)
Traceback (most recent call last):
    2: from ./step09.rb:116:in `<main>'
    1: from ./step09.rb:116:in `new'
./step09.rb:7:in `initialize': Float is not supported. (TypeError)
  • ステップ10:テストを作る

Rubyのテスト環境の構築方法はあまり詳しくないのだが、昔の資料を取り出してきて同じようにテスト環境を構築した。assert_equalによって想定する値と一致するかどうかをチェックする。

  • test10.rb
#!/usr/bin/env ruby

require 'test/unit'

...
    
class TestDezero < Test::Unit::TestCase
  def test_backward
    x = Variable.new([3.0])
    y = square(x)
    y.backward()
    assert_equal [6.0], x.grad
  end

  def test_gradient_check
    x = Variable.new([rand])
    y = square(x)
    y.backward()
    num_grad = numerical_diff(method(:square), x)
    flg = x.grad.zip(num_grad).map{|x,y|
      (x - y).abs / x.abs <= 1e-08
    }.all?
    assert_equal flg, true
  end
end

やはりNumPyには便利な機能があって、「ほとんど近しい」ということを示すためのnp.allclose()という関数が存在しているのだがこんなものRubyには存在しないので、無理やり差分の絶対値の比率が1e-08よりも小さくなることを確認するコードに変換した。これで自動チェックが行われるようになった。

./step10.rb 
Loaded suite ./step10
Started
..
Finished in 0.0026976 seconds.
---------------------------------------------------------------------------------------
2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
---------------------------------------------------------------------------------------
741.40 tests/s, 741.40 assertions/s