2017年6月24日土曜日

Ruby on Rails Active Recordのソースコードリーディング

調べた問題

ActiveRecordでCompanyモデルに紐づくUserという物があったときに、関連のあるUserを作成するときにbuildを使って以下のようにする。その際、まだDBには保存されていないUserが次の検索ででくるか気になったので実験しつつ、ActiveRecordのソースを調べてみた。

Company.users.build(name: 'xxxxx')


調査

railsのソースコードリーディングについてここ参考にさせてもらった。AZS

上を参考にしつつ、railsのコードを落としてコードリーディング用のプロジェクトを作成した後、モデルを作成して、DBにテーブルを作る。

rails g model Company name:string
rails g model User company_id:integer name:string

bundle exec rails db:migrate RAILS_ENV=development
== 20170617020543 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0027s
== 20170617020543 CreateUsers: migrated (0.0028s) =============================

== 20170617020644 CreateCompanies: migrating ==================================
-- create_table(:companies)
   -> 0.0010s
== 20170617020644 CreateCompanies: migrated (0.0015s) =========================

そしてCompanyにCompanyとUserの関連を持たせるのと、コードを追うためのメソッドを追加。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    com.users.where(name: 'test_company')
  end
end

実行して追いかけてみる。buildはcollection_proxy.rbのbuildメソッドが呼ばれているもよう。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_proxy.rb @ line 316 ActiveRecord::Associations::CollectionProxy#build:

    315: def build(attributes = {}, &block)
 => 316:   @association.build(attributes, &block)
    317: end

 @associationの実態は以下のようActiveRecord::Associations::HasManyAssociationというクラスのインスタンスでフィールドにCompanyとUserに関する情報をいろいろ持っている。


[1] pry(#<User::ActiveRecord_Associations_CollectionProxy>)> @association
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = ?  [["company_id", 1]]
=> #<ActiveRecord::Associations::HasManyAssociation:0x007f752027be50
 @association_scope=nil,
 @inversed=false,
 @loaded=false,
 @owner=#<Company:0x007f753ec2a668 id: 1, name: "test_company", created_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00, updated_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00>,
 @proxy=[],
 @reflection=
  #<ActiveRecord::Reflection::HasManyReflection:0x007f753f044988
   @active_record=Company(id: integer, name: string, created_at: datetime, updated_at: datetime),
   @active_record_primary_key="id",
   @association_scope_cache=
    {true=>
      #<ActiveRecord::StatementCache:0x007f753efcb060
       @bind_map=
        #<ActiveRecord::StatementCache::BindMap:0x007f753efcb5d8
         @bound_attributes=
          [#<ActiveRecord::Relation::QueryAttribute:0x007f753efc5b10
            @name="company_id",
            @original_attribute=nil,
            @type=#<ActiveModel::Type::Integer:0x007f753efa4a00 @limit=nil, @precision=nil, @range=-2147483648...2147483648, @scale=nil>,
            @value=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>,
            @value_before_type_cast=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>>],
         @indexes=[0]>,
       @query_builder=#<ActiveRecord::StatementCache::Query:0x007f753efcb088 @sql="SELECT \"users\".* FROM \"users\" WHERE \"users\".\"company_id\" = ?">>},
   @automatic_inverse_of=false,
   @class_name="User",
   @constructable=true,
   @foreign_key="company_id",
   @foreign_type="users_type",
   @klass=User(id: integer, company_id: integer, name: string, created_at: datetime, updated_at: datetime),
   @name=:users,
   @options={},
   @plural_name="users",
   @scope=nil,
   @scope_lock=#<Thread::Mutex:0x007f753f044618>,
   @type=nil>,
 @stale_state=nil,
 @target=[]>

Userのインスタンス化自体は、このパターンだとObjectのnewが使われていた。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/inheritance.rb @ line 65 ActiveRecord::Inheritance::ClassMethods#new:

    48: def new(*args, &block)
    49:   if abstract_class? || self == Base
    50:     raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
    51:   end
    52:
    53:   attrs = args.first
    54:   if has_attribute?(inheritance_column)
    55:     subclass = subclass_from_attributes(attrs)
    56:
    57:     if subclass.nil? && base_class == self
    58:       subclass = subclass_from_attributes(column_defaults)
    59:     end
    60:   end
    61:
    62:   if subclass && subclass != self
    63:     subclass.new(*args, &block)
    64:   else
 => 65:     super
    66:   end
    67: end

[9] pry(User)> self.parent
=> Object


こので戻ってきたメソッドないでは、Userがインスタンス化されている。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 281 ActiveRecord::Associations::CollectionAssociation#add_to_target:

    277: def add_to_target(record, skip_callbacks = false, &block)
    278:   if association_scope.distinct_value
    279:     index = @target.index(record)
    280:   end
 => 281:   replace_on_target(record, index, skip_callbacks, &block)
    282: end

[11] pry(#<ActiveRecord::Associations::HasManyAssociation>)> record
=> #<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>


before_addとかafter_addはここで呼ばれているのか、へー


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 441 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
 => 441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
    452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

targetというArrayにこのレコードを入れているが、最終的にこれがCompanyと関連をもって保存されるのかな?


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 452 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
    441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
 => 452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

[17] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target
=> []
[19] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target.class
=> Array

この次はもう呼び出し元へ戻ってきて、usersの中身は入ってた。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 9 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
 =>  9:   com.users.where(name: 'test_company')
    10: end

[24] pry(Company)> com.users
=> [#<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>]

whereの実態はココらへん

From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 600 ActiveRecord::QueryMethods#where:

    599: def where(opts = :chain, *rest)
 => 600:   if :chain == opts
    601:     WhereChain.new(spawn)
    602:   elsif opts.blank?
    603:     self
    604:   else
    605:     spawn.where!(opts, *rest)
    606:   end
    607: end



From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 610 ActiveRecord::QueryMethods#where!:

    609: def where!(opts, *rest) # :nodoc:
 => 610:   opts = sanitize_forbidden_attributes(opts)
    611:   references!(PredicateBuilder.references(opts)) if Hash === opts
    612:   self.where_clause += where_clause_factory.build(opts, rest)
    613:   self
    614: end

その後 self.build_users へ戻ってきてしまったので、com.users.where(name: 'test_company')の段階では検索の条件などを組み立てるだけで、実際の値の検索はしていないようなので(遅延評価ってやつかな?)ちょっとコードを変えて再実行。

以下のように最後にwhereで組み立てたusersをカウントすることによって、どこの値をみてるかわかるはず。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    users = com.users.where(name: 'test_company')
    _count_users = users.count
  end
end
~



追ってくとこんなところがあって、SQLを組み立ててるのでDBに取りに行く模様


From: /home/ishioka/repos/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @ line 34 ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all:

    31: def select_all(arel, name = nil, binds = [], preparable: nil)
    32:   arel, binds = binds_from_relation arel, binds
    33:   sql = to_sql(arel, binds)
 => 34:   if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
    35:     preparable = false
    36:   else
    37:     preparable = visitor.preparable
    38:   end
    39:   if prepared_statements && preparable
    40:     select_prepared(sql, name, binds)
    41:   else
    42:     select(sql, name, binds)
    43:   end
    44: end

[2] pry(#<ActiveRecord::ConnectionAdapters::SQLite3Adapter>)> sql
=> "SELECT COUNT(*) FROM \"users\" WHERE \"users\".\"company_id\" = ? AND \"users\".\"name\" = ?"


結局SQLが発行されてDBの値が検索されてそれが検索される。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 10 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
     9:   users = com.users.where(name: 'test_company')
 => 10:   _count_users = users.count
    11: end

[1] pry(Company)> n
   (0.8ms)  SELECT COUNT(*) FROM "users" WHERE "users"."company_id" = ? AND "users"."name" = ?  [["company_id", 1], ["name", "test_company"]]

From: /home/ishioka/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/pry-0.10.4/lib/pry/pry_instance.rb @ line 356 Pry#evaluate_ruby:

    351: def evaluate_ruby(code)
    352:   inject_sticky_locals!
    353:   exec_hook :before_eval, code, self
    354:
    355:   result = current_binding.eval(code, Pry.eval_path, Pry.current_line)
 => 356:   set_last_result(result, code)
    357: ensure
    358:   update_input_history(code)
    359:   exec_hook :after_eval, result, self
    360: end

[1] pry(#<Pry>)> c
=> 0


結論

whereでは検索条件が作成されるだけであって、まだ検索はされない。
実際にその値が必要になった場合に、「DBから」検索されるので、紐付いているオブジェクトは検索結果には入ってこない。

最後に

ActiveRecordのコードを少し追っかけてみたが、結構複雑で呼び出し順や、各オブジェクトの関係などすぐには理解できなそう。
ただすごい参考になりそう(rubyの使い方、設計思想など)なので、issueでも拾ってコードリーディングしてみるかな。

2017年6月6日火曜日

Jupiter Notebook + Keras(Tensor Flow)でチュートリアルをしてみる4 MINIST

Kerasチュートリアルの第4弾、今回はConvolutional Neural Networkここも参考になった)を組んで精度を上げてみる。ここページを参考にしている。

まず必要なライブラリをインポートする。
いろいろ見た事ないものがあるので、調べてみると、
Flattenは2次元のデータを1次元のベクターにするのに使うらしい。
Conv2Dは2次元配列の畳み込み演算するのに使う。
MaxPooling2Dは、画像の圧縮をするのに使う。

import numpy
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras import backend as K
K.set_image_dim_ordering('th')


そしてお馴染みのランダムにシードの設定

# fix random seed for reproducibility
seed = 7
numpy.random.seed(seed)


第1層がConv2Dになるのだが、そこに読み込める形にデータを整形する。
KerasのConv2Dに入力するためには、[画像数][pixles(グレースケールは1)][width][height]の4次元配列にする必要があるのでshapeを使う。

# load data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
# reshape to be [samples][pixels][width][height]
X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28).astype('float32')



前回もやった0 - 255を0 - 1にNormalizeと教師データの整形を今回もやる。

# normalize inputs from 0-255 to 0-1
X_train = X_train / 255
X_test = X_test / 255
# one hot encode outputs
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]



モデルの定義をする。
1層目はConv2Dで32フィルター、フィルタのサイズ5 x 5、input_shapeは[1(グレースケール)][width][height]で指定。

2層目はMaxPooling2Dを使って画像を圧縮。slidesは指定がなければ、pool_sizeと同じになるようなので、2 x 2の領域が1つのPixcelにされるので14 x 14 の配列になるはず。

3層目はDropoutを使って20%のneuronsを伝播させない。これを入れるとover fittingを避けられるらしい。

4層目は2次元配列を1次元のベクターに変更

5層目は全結合のレイヤーで128 output

6層目は0 - 9の10のoutputでsoft max関数で確立に変換している。

# create model
model = Sequential()
model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
# Compile model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])


そしてモデルの学習。前回と同じように200サンプルごとにパラメタのアップデートして、10回繰り返し。

# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, batch_size=200, verbose=2)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Baseline Error: %.2f%%" % (100-scores[1]*100))



最終出力は以下。おお、エラー率が1%以下になっている。99%は判別できるという事。素晴らしい。


Train on 60000 samples, validate on 10000 samples
Epoch 1/10
172s - loss: 0.2310 - acc: 0.9347 - val_loss: 0.0825 - val_acc: 0.9743
Epoch 2/10
170s - loss: 0.0736 - acc: 0.9780 - val_loss: 0.0467 - val_acc: 0.9841
Epoch 3/10
175s - loss: 0.0531 - acc: 0.9839 - val_loss: 0.0432 - val_acc: 0.9856
Epoch 4/10
165s - loss: 0.0401 - acc: 0.9878 - val_loss: 0.0406 - val_acc: 0.9869
Epoch 5/10
179s - loss: 0.0337 - acc: 0.9893 - val_loss: 0.0346 - val_acc: 0.9886
Epoch 6/10
156s - loss: 0.0275 - acc: 0.9916 - val_loss: 0.0309 - val_acc: 0.9893
Epoch 7/10
163s - loss: 0.0232 - acc: 0.9927 - val_loss: 0.0359 - val_acc: 0.9877
Epoch 8/10
158s - loss: 0.0204 - acc: 0.9937 - val_loss: 0.0324 - val_acc: 0.9887
Epoch 9/10
164s - loss: 0.0166 - acc: 0.9947 - val_loss: 0.0300 - val_acc: 0.9901
Epoch 10/10
178s - loss: 0.0143 - acc: 0.9958 - val_loss: 0.0310 - val_acc: 0.9905
Baseline Error: 0.95%


ちなみに今回構築したモデルを可視化すると以下のようになっていた。







2017年6月3日土曜日

Jupiter Notebook + Keras(Tensor Flow)でチュートリアルをしてみる3 MINIST

Kerasチュートリアル第3弾。

機械学習のモデルを評価する時によく使われる、MINIST(手書き文字の認識をする)問題のコードを追っていく。
解説している記事をみながら、コードを追ってみる。

使う画像のサイズは28 x 28ピクセル。
60000イメージはモデル構築のために、10000イメージはテストのために使う。

いいモデルはエラー率1%以下になるらしい。

では実際にコードを見ていく。

実際の文字イメージ画像群(データセット)をダウンロードする必要があるが、Kerasでは便利なスクリプトが用意されているらしい。(~/.keras/datasets/mnist.pkl.gz にダウンロードされる)

以下はダウンロードしてプロットするスクリプト。

# Plot ad hoc mnist instances
from keras.datasets import mnist
import matplotlib.pyplot as plt
# load (downloaded if needed) the MNIST dataset
(X_train, y_train), (X_test, y_test) = mnist.load_data()
# plot 4 images as gray scale
plt.subplot(221)
plt.imshow(X_train[0], cmap=plt.get_cmap('gray'))
plt.subplot(222)
plt.imshow(X_train[1], cmap=plt.get_cmap('gray'))
plt.subplot(223)
plt.imshow(X_train[2], cmap=plt.get_cmap('gray'))
plt.subplot(224)
plt.imshow(X_train[3], cmap=plt.get_cmap('gray'))
# show the plot
plt.show()


ふむ、Jupiter Notebook上でも綺麗に表示された。

ではデータはダウンロードされたので、今回使うライブラリたちを読み込む。
Dropoutというのは、過学習を避けるためにランダムに伝播をさせずに、ネットワークをFatにさせないテクニックらしい。
詳しい論文

keras.unilsはその名の通り、kerasの便利クラス。モデルのシリアライズしたりするメソッドがある。https://keras.io/utils/


import numpy
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.utils import np_utils



前回と同じように、再現性を持たせるためにseedをセット。

# fix random seed for reproducibility
seed = 7
numpy.random.seed(seed)



でMINISTのデータを読み込む

# load data
(X_train, y_train), (X_test, y_test) = mnist.load_data()


読み込んだトレーニングセットは3次元の配列(白黒の場合)だが、モデルに食わせるためには、Vector(1次元の配列)にする必要があるらしい。28 x 28の画像だから784要素の配列する。

# flatten 28*28 images to a 784 vector for each image
# print(type(X_train))
num_pixels = X_train.shape[1] * X_train.shape[2] # 28 * 28
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32') # 60000 , 784
# 784要素のArrayが60000個ある2次元配列になった。
# print(X_train.shape)
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')


グレースケールは0 to 255で表しているが、これを255で割って0 to 1のスケールにする。

# normalize inputs from 0-255 to 0-1
X_train = X_train / 255
X_test = X_test / 255


教師データのラベルは0,1,2,3と正解の数値で表されているが、これを
0だったら [1,0,0,0,0,0,0,0,0,0]
5だったら [0,0,0,0,1,0,0,0,0,0]
と表すように変換する。

# one hot encode outputs
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]


ついにモデルの定義をする。
1層目は入力784、出力784
2層目は入力784、出力10、活性化関数にsoftmaxを使っている。ざっくり言うと出力を確率に変えてくれるらしい。

損失関数はcategorical_crossentropy = Logarithmic loss


# create model
model = Sequential()
model.add(Dense(num_pixels, input_dim=num_pixels, 
                kernel_initializer='normal', activation='relu'))
model.add(Dense(num_classes, kernel_initializer='normal', activation='softmax'))
# Compile model
model.compile(loss='categorical_crossentropy', 
              optimizer='adam', metrics=['accuracy'])


データを与えて、モデルを最適化する
200イメージごとにモデルのパラメータをアップデートして、20回繰り返す。

最後にテストデータでテストしたスコアをプリント!

# Fit the model
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=20, batch_size=200, verbose=2)
# Final evaluation of the model
scores = model.evaluate(X_test, y_test, verbose=0)
print("Baseline Error: %.2f%%" % (100-scores[1]*100))




Baseline Error: 1.61%

が最終出力。つまり10000のイメージでテストしてみて、1.61%はこのモデルでの予想とはずれたが、98.4%は正解になったということ

こんなシンプルな2層のネットワークでも98.4%正解になると驚きだった。まあ人間がやれば99.9%ぐらい成功できるだろうけど。

ただこのチュートリアルはシンプル 編なようなので、次週もっと正解率を高めるモデル構築を見ていく。

たぶん使ってないDropoutを使うんだろうな。