studylog/北の雲

chainer/python/nlp

可変長のミニバッチ、softmax_cross_entropyに-1、EmbedIDの謎挙動など

ほとんどChainer専用ブログになってきたのでタイトルにChainerメモって付けるのやめました。

https://groups.google.com/forum/#!topic/chainer/VFbZCgccs6I
ここで少し言及されていたので。

[明日 晴]
[今日 は 雨]
[本当 に 凄い 雪]

こういう可変長をミニバッチで学習させる時に長さが違うので困る。
[0 1]
[2 3 4]
[5 6 7 8]
次元が合わない。

なので-1で埋めて長さを合わせる。
[0 1 -1 -1]
[2 3 4 -1]
[5 6 7 8]

これをひっくり返して

[0 2 5]
[ 1 3 6]
[-1 4 7]
[ -1 -1 8]

こうしてからRNNに投げる
最初は
[0 2 5] これがx
[ 1 3 6] これがt(正解)
これは問題ない。

次は一つずらして
[ 1 3 6] これがx
[-1 4 7] これがt(正解)

ここで正解データに-1が登場する。
chainer1.4からはsoftmax_cross_entropyの正解(t)に-1を投げるとそれを「無視」してくれるようになった。

http://docs.chainer.org/en/stable/reference/functions.html?#chainer.functions.softmax_cross_entropy

t (Variable) – Variable holding an int32 vector of groundtruth labels. If t[i] == -1, correspondig x[i] is ignored.

Chainer1.4.0がリリースされました - studylog/北の雲 (一番最後)

「無視」というのが正確にどうなってるのか中のコードを読んでないのでわからないけど、ようする計算しない?と解釈してます。ここまでは問題ない。

そんで次は色々と問題が出てくる。
[-1 4 7] x
[ -1 -1 8] t

xに-1が含まれてしまう。
これやっちゃうとどうなるかというと、EmbedIDに-1を投げてることになる。存在しないはずの-1なんだけど、なぜか普通にWなどの値が返ってきたりする。たまにnanの時もある。CPUだと何らかの値が返ってくる事が多いけどGPUだとnan率が高いなどと、まちまち。いずれにせよ-1は存在しないはずなのにエラーが出ないので見かけ上はそのまま学習が進む。
でもGPUだとすぐ破綻してlossがnanになる。CPUだとなぜかnanにはならない。よくわからない。
とにかくxに-1を含めちゃいけない。めちゃくちゃになる。
(EmbedIDに-1投げたらエラーが出るべきなのかなと思ってますが…。)

この回避策としてxに-1が含まれてたら自分はEOSに該当するidに変えるようにしている。これだと弊害が少ないはず。EOSのvocab_idが100だとしたら
[-1 4 7] x
[ -1 -1 8] t

[100 4 7] x
[ -1 -1 8] t
に変えてから学習してる。これでちゃんと学習できるようになったけどモヤモヤする。本当はこうしたい。
あおのたす on Twitter: "任意長のデータをchainerのLSTMでミニバッチでエンコードしたい場合、どうすればいいのだろうか…0埋めするとエンコードした場合最終的なhがzero vectorになってしまう。" (会話の下の方)
https://groups.google.com/forum/#!topic/chainer/M7ea5yAjdWM
でも意味がよくわからないので保留してる。
追記:解決しました

ついでにEmbedIDの謎挙動

n_vocab=10でEmbedID作って、

  • 10を超えた場合

CPU すぐエラーが出る。(IndexError: index 10 is out of bounds for size 10)
GPU 超えてもかなり大丈夫、いつかこれでとまる (cupy.cuda.runtime.CUDARuntimeError: cudaErrorIllegalAddress: an illegal memory access was encountered)

  • マイナス値

CPU、GPUともにエラーでない 全部nanで埋まってたりすることもある

これが正しい挙動なのかバグなのかはよくわからないけど、気をつけないと危険。

ついでにClassifierのaccuracy

Classifier使ってると勝手にaccuracyを計算してくれて便利なんだけど、tが-1の時は不正解と見なされるようで困ってる。


chainerに対して色々と気づく事はあるのだけど、英語できないと報告する術が無いのが辛いです…。githubに日本語で書くわけにはいかないし、日本語フォーラムができたからそっちに書けばいいのかな?

おしまい。

PycharmのremoteインタプリンタでchainerのGPUを実行時にNo such file or directory: 'nvcc'

Pycharmのremoteインタプリンタは便利。ローカルマシンでコード書いて、リモートのUbuntuで実行するときにPycharm内で全て完結する。いちいちterminalで実行しなくていい。

…はずなんだけどchainerをGPUで動かすと

OSError: Failed to run `nvcc` command. Check PATH environment variable: [Errno 2] No such file or directory: 'nvcc'

というエラーが出ちゃうのでGPUの時だけはterminalで実行してた。pycharmのデバッグも使えないし面倒だなあと思ってたけど解決したのでメモ。

原因:
リモートからpython実行するときにPATHがCUDAに通っていなかった模様。

print(os.environ["PATH"])  #=>cudaにパスが通ってないPATHが表示
os.environ['PATH'] += ':/usr/local/cuda-7.0/bin:/usr/local/cuda-7.0/bin'  #CUDAに通す
print(os.environ["PATH"])  #CUDAへのパスが追加された

おわり。

Chainer1.5の変更点についてのメモ

あけましておめでとうございます。今年もChainer中心に遊んでいきたいと思います。

過去最大のアップデート 1.5。あまりにも変わりすぎていて以前のコードを1.5対応に書き換えるが結構大変です。
正直1.4のままでいいかな…と思いましたが、最新版についていかないと損しそうなので箱根駅伝見ながら勉強しました。1.5でCuPyがだいぶ速くなったようですし。

未解決なところ、曖昧・あやふやな情報がありますがご了承下さい。一部1.4以前に既にあったものも含まれてるかもしれません。
自信が無いところはこの色で書いてます。

まずは1.5版チュートリアル和訳から、後はサンプルコードとドキュメントを見ながら試行錯誤しました。
i101330.hatenablog.com

一番変わったのがこれ。重みなどのパラメータがあるのがLink、無いのがFunctionなのかな。まだ自分もイマイチわかってないです。特にChain…。再利用性が高まったらしいですがそういう高度な事は自分には理解できないのでもう少し勉強してここは加筆したいところ。

__call__

最初に躓いたのがここ。forwardじゃなくて__call__でやる流儀になったそうです。何年もpython触ってるんですが__call__なんて初めて使いました。__call__のおかげで、

loss = model.forward( x , t )
こんな感じだったのが
loss = model(x , t)

とスタイリッシュに。でもぱっと見なにをやってるのか分かり辛くなった気がします。

WeightDecay、GradientClippingはadd_hookで

以前は以下のように学習ループ中に記述してました。

loss.backward()
optimizer.weight_decay(0.0001) #ここ
optimizer.update()

1.5では

optimizer = optimizers.Adam()
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001)) #ここ

こうやってoptimizerの初期化後に一度だけ指定するようになりました。学習ループ中に指定するとエラーでます。一度だけでいい模様。optimizer._hooksに記録されます。
古い書き方でもエラーは出ませんがduplicatedだそう。古い書き方だとそもそも有効にならないのかな?不明。

ハマりポイントになりそうなので書いておきますが、optimizerを保存(serialize)してもadd_hookの情報は記録されない模様。自信は無いですがoptimizer._hooksが空になってるんですよね。これは後でまた書きます。

optimizer.update()

Recurrent Nets and their Computational Graph — Chainer 1.5.1 documentation
ここによると

def compute_loss(x_list):
    loss = 0
    for cur_word, next_word in zip(x_list, x_list[1:]):
        loss += model(cur_word, next_word)
    return loss
model.zerograds()
loss = compute_loss(x_list)
loss.backward()
optimizer.update()

これを

optimizer.update(compute_loss, x_list)

と短く書けるようです。
これもぱっと見どこで学習してるのか分かり辛くなる可能性があるので自分は従来の書き方でいこうと思います。
また短い書き方だとRNNでloss.unchain_backward()が使えない?
lossはどこから取ればいいのか?(グラフ描画とかに使いたい)
という疑問も。フォーラムで聞いてみたい。

モデルの公式save/load

1.5目玉の一つ。モデル保存時はGPUでもGPUの方のメモリを使わない?これは嬉しい

load時に注意するところがいくつかあります。ちゃんとやらないとハマります。モデル読み込んだ後に学習再開してlossが増えちゃった場合はsave/loadが適切に行われていない可能性大。

失敗した実例。
学習中断時はlossが6300付近で順調に下がっていた。以下のグラフは再開後のlossですがいきなり急上昇して結局元のlossに戻るまで数時間かかってしまった。
f:id:kitanokumo:20160105203328p:plain
こんな風になっちゃってる人は保存読み込みを失敗してます。

保存

#初期化
lm = net.RNNLM(n_units = 512) #RNNLMは自作ネットワーク
model = L.Classifier(lm)
#ここ要注意
model.compute_accuracy = False  # accuracyが必要ない場合はFalseに Falseにした方が学習が速い?

# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))

〜ここで学習して〜

#save
#modelとoptimizerの二つ保存する必要あり
serializers.save_hdf5('test.model', model)
serializers.save_hdf5('test.state', optimizer)

#GPU/CPU共通の保存方法で大丈夫です
#to_gpuとかto_cpuしなくてよし 保存の時に圧縮率を指定できる  やり方はドキュメントを

読み込み

#初期化
lm = net.RNNLM(n_units = 512) #RNNLMは自作ネットワーク
model = L.Classifier(lm)
#ここ要注意
model.compute_accuracy = False  # 再度指定しないと引き継がれない

# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model) #ここでsetupする 一度だけする
#ここ要注意
#add_hookはserializeされない?ので再指定しないといけないっぽい
#指定しないとoptimizer._hooksが空のままになる
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))  # 再度指定しないと引き継がれない

#load
serializers.load_hdf5('2ch_model_15_2.model', model)
serializers.load_hdf5('2ch_model_15_2.state', optimizer)

#なんとなくmodelを読み込んでからもう一度setupした方がいいような気がしたので
#ここでもう一度setupしちゃってたけどこれは駄目
#optimizerの一部の情報がリセットされちゃいます かなりはまった
#optimizer.setup(model)   これは駄目 やらなくていい

読み込み時のsetup、add_hook、modelのcompute_accuracyなどに注意ですね。
add_hookは明確に指定しないとoptimizerの_hooksが空のままになってるので無効になっちゃってる模様。自信は無い。後でフォーラムで聞いてみようと思います。

1.4以前のモデルを1.5にアップデートする

Capitalicoでのchainer 1.1 → 1.5 バージョンアップ事例 のP18〜
https://groups.google.com/forum/#!topic/chainer/eXyL11thcNY

この辺を参考に。自分はやってません。

Classifier

これを使うとlossやaccを管理しなくていい。隠遁される。

model = L.Classifier(MyChain(), lossfun=mean_squared_error)

lossfunはデフォルトではsoftmax_cross_entropy。
lossやaccの値はmodel.lossなどで取れます。

ただ簡潔・洗練されすぎて他人の書いたコードの場合何をやってるのか一目でわかりにくいかも。

optimizerのtとepoch(1.4以前からあったかも)

optimizer.t #これはoptimizer.updateした回数が自動的に入る模様
optimizer.epoch #これは手動でnew_epoch()を呼ばないと0のまま?自動でインクリメントされない

volatileがon offに?

x = chainer.Variable(xp.asarray(x),  volatile='on’)

従来のBoolean(True False)でもいける模様。なぜこうなったのかよくわからない。自分はtrainにTrue Falseを入れてそれをnotする方が好きなので従来のやり方で。

Flagってのが新しく追加されてるけどこっち使うのかな?サンプルでは使われていなかった。
http://docs.chainer.org/en/stable/reference/core/flag.html

Linkにxp(cuda.cupy or numpy)が自動的に入る模様

type_check_enableが有効になってるかの確認

参考:Chainerメモ13 type_checkをオフにして高速化 - studylog
無効の方が学習が速い。

Linkにtypecheckは無いようなのでFunctionで
print(F.Concat.type_check_enable)

こんなのじゃなくて専用の確認方法がある気がする。知ってる人教えてください。

初期重みをランダムで設定

#1.4まで
for param in model.parameters:
    param[:] = np.random.uniform(-0.1, 0.1, param.shape)

#1.5
for param in model.params():
    data = param.data
    data[:] = np.random.uniform(-0.1, 0.1, data.shape)

デフォルトでもそれなりに小さい値をセットしてくれてます。
n_unitsが大きくなるほど重みは小さい値になる模様。
デフォルトではbiasは0になってるので、上記のように自分で設定しない方がいいのかも(青い深層学習本によると初期biasは0がいいらしい)。

参考

#Linearでの初期化コード
    def __init__(self, in_size, out_size, wscale=1, bias=0, nobias=False,
                 initialW=None, initial_bias=None):
        super(Linear, self).__init__(W=(out_size, in_size))
        if initialW is None:
            #ここでランダムに設定 in_sizeの大きさによって上限値が変わってる?
            initialW = numpy.random.normal( 0, wscale * numpy.sqrt(1. / in_size), (out_size, in_size))
        self.W.data[...] = initialW

以上です。間違いがあれば指摘してください。よろしくお願いします。

(追記)

LSTMのreset_state

サンプルではLSTMの層を指定してreset_stateしている(以下ptbサンプル)。

    def __init__(self, n_vocab, n_units, train=True):
        super(RNNLM, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l1=L.LSTM(n_units, n_units),
            l2=L.LSTM(n_units, n_units),
            l3=L.Linear(n_units, n_vocab),
        )
        self.train = train

    def reset_state(self):
        self.l1.reset_state() #ここでl1と指定
        self.l2.reset_state()

これだとLSTMの層を増やしたときにreset_stateでの指定漏れの恐れがある。例えばl5まで作ったときにreset_stateを書き換えないとl3,l4がreset_state()されないけどエラーも出ないので気づかない(減らしたときはエラーが出る)。実際それで数時間無駄にしてしまったので、層を増やしても自動的に検知してreset_stateしたいと思って次のようにしてみた。

    def __init__(self, n_vocab, n_units, train=True):
        super(RNNLM, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l1=L.LSTM(n_units, n_units),
            l2=L.LSTM(n_units, n_units),
            l3=L.Linear(n_units, n_vocab),
        )
        self.train = train
        #LSTMの層だけ入れておく もっと賢い入れ方ありそう
        self.lstm_layers = [ getattr(self, c) for c in self._children if isinstance(getattr(self, c), chainer.links.LSTM)]

    #ここで自動的にLSTMを検知してreset
    def lstm_reset_state(self):
        for layer in self.lstm_layers:
            getattr(layer , "reset_state")()

ディープラーニング用ライブラリの仁義なき争い勃発

GoogleがTensorFlowをリリースしてちょっとした騒ぎになっています。
tensorflow/tensorflow · GitHub

GitHubに付けられたStarは既に6800。
それがどのくらい凄いかというと、3ヶ月前の時点でこんな感じの情勢だったんです。

左は5月、右が8月のStar数。
Caffeがダントツで5600、Kerasが2500で二番手に躍り出て、chainerもリリース二ヶ月で600台と順調に伸びてる。
ちなみにこれつぶやいてるのはKeras作ってる人。Googleの中の人らしい。たまに日本語つぶやくので日本人なのかな?

そういう状況がほとんど一夜にしてがらっと変わりました。
TensorFlowは既に6800。えー。


まあStar数なんて目安でしか無いわけだけど、それでも注目度というか衝撃度の凄さがわかります。
ライブラリを作ってる人たちのtwitterを見てると同様が隠しきれない感じが伺える。


さて今後はどうなっていくんでしょうか。
いまいちまだTensorFlowが何なのかわかってないんですが、Theanoを凄い勢いで置き換えていくんでしょうかね。

そんで、「ディープラーニングの総合ライブラリ」みたいなのは徐々に死滅していくんじゃないかと思ってます。
もっと分野・タスクに特化したライブラリに進む事でお互いに差別化を図って生き残っていくような。
NLPタスクなら任せとけ!RNN界隈の論文は速攻で取り入れるぞ!」みたいな。

画像物体認識とか全く自分は興味がわかないので自然言語タスクに特化したのがあればなあといつも思ってて。
LSTMにもTreeとかN-dimensionalとか亜種・改良版がどんどん提案されてるのでそれをすぐに取り入れてくれるようなライブラリ。
ライブラリ本体の機能じゃなくてもよくて、とにかく話題になった論文はすぐそのライブラリでのコードが公開されるような所が生き残って行くような気がする。

chainerさんはどういう方向に行くんでしょうか。

おしまい

ニコニコ静画の学習済みchainerモデルを試してみた


を試してみました。

モデルファイルは上のnico-opendataより、必要なライブラリとコードはこちらから。github.com
モデルファイルは申請(規約に同意するだけ)してすぐダウンロードできます。約1GB。研究目的なら誰でもOKだそうです。


chainer1.3(1.4でも動きました)、pillow、numpy、argparseが必要。これが既に入っている人は別途

sudo pip install git+http://github.com/nico-opendata/niconico_chainer_models.git#egg=niconico_chainer_models

だけでOKです。
python2.7で動作確認済み、3系ではモデルファイルを読み込めませんでした。

学習済みモデルを使って予測するだけなのでCPUでも一瞬ですが、モデルファイルは解凍すると2.6GBになるのでそれ以上メモリが無いと試せないと思います。


AttributeError: addinfourl instance has no attribute 'seek'
とエラーが出る人はこれでpillowを新しいのに更新してみてください。

sudo pip install pillow -U

ダウンロードして解凍したファイル群にあるcharacter_series.txtのタグが推定されるようです。
どうも最近のアニメのキャラクターが中心のようで、アニメ=ドラゴンボール幽遊白書なアラサーには名前がほとんどわかりません。
ワンピース、ドラえもんアンパンマンなどの大御所はカバーされていました。ジャイアンはいない。ニコニコ静画に投稿された主にイラストデータが元なので、いわゆる同人的な創作活動によく使われるキャラがメインっぽいです。いまどき悟空なんて描く人いないんだろうな。

http://files.hangame.co.jp/blog/2009/43/27058597/01/03/18994571/27058597_1230980411008.jpg
こちらの画像で試してみるとこう出力されました。

tag: レミリア / score: 1.0
tag: レミリア・スカーレット / score: 1.0
tag: 東方 / score: 1.0
tag: 永江衣玖 / score: 1.0

正解は東方のレミリアスカーレットさん。
ただ永江衣玖というタグも出力しちゃっている。調べてみると永江衣玖レミリアスカーレットの差がよくわからない。似てる。コウモリみたいな羽がスカーレットなのでしょうか。

永江衣玖で検索して出てきたこの画像だと、
http://blog-imgs-43-origin.fc2.com/r/a/k/rakueru/Tohou-Iku16.jpg

tag: 東方 / score: 1.0
tag: 永江衣玖 / score: 1.0
tag: 比那名居天子 / score: 8.98948082639e-10

と見事に正解。

初音ミク
http://internet.watch.impress.co.jp/img/iw/docs/611/175/miku06.png

tag: Lat式ミク / score: 1.0
tag: VOCALOID / score: 1.0
tag: 初音ミク / score: 1.0


タグにドラクエ関係もあったので試してみる。
http://www.jp.square-enix.com/dqsp/dq2/images/main_visual.jpg

tag: ドラゴンクエストX / score: 1.0
tag: 東方 / score: 0.999966025352

学習データが同人イラストなので元のオリジナル画像よりも同人画像の方がうまく分類できるのかもしれません。その辺りが好きな人にとっては楽しめるモデルになりそうです。

chainer初の大規模学習済みモデルの公開なのかな。
ありがとうドワンゴ

おしまい。

Chainerメモ13 type_checkをオフにして高速化

少し前から気になっていたのですが1.3.0よりtype_checkをオフにすることで処理を高速化できるようになっていました。


Function — Chainer 1.4.0 documentation

type_check_enable
When it is True, the function checks types of input arguments. Set CHAINER_TYPE_CHECK environment variable 0 to disable type check, or set the variable directly in your own program.


やってみました。このやり方でいいのかどうかは不安です。

import os
os.environ["CHAINER_TYPE_CHECK"] = "0" #ここでオフに  オンにしたかったら1にするかコメントアウト

#以下chainer関係のimportなど
import chainer 
省略...

#これがFalseになっていればOK
print(chainer.functions.Linear(1,1).type_check_enable) 

chainerのimportよりも上に書かないといけないようです。
下に書いちゃうと反映されないようで、print(chainer.functions.Linear(1,1).type_check_enable)がTrueになっちゃいます。

本当に速くなるのか

速くなりました。
LSTMを使ったRNN言語モデル構築コードで104%ほど。
処理によってどのくらい速くなるかは変わるはずですが、確かに速くなるようです。

1.開発時はtype_checkをオンに(デフォルトでオン)
2.とりあえず1epoch回してみるところまでオンでやる
3.問題なければオフにして速度重視で本番モデルを構築する
という流れでしょうか。

もしかしたら弊害もあるかもしれませんのでお仕事で使われている方はあまりこの記事を鵜呑みにせずちゃんと調査して欲しいです。
おわり。

Chainer1.4.0がリリースされました

主にバグ修正、CuPy関係の機能追加のようです。
モデル構造変更、公式保存方法などの実装は一ヶ月後の1.5.0になりました。

主な変更点はこちら
https://groups.google.com/forum/#!topic/chainer/bnfSQpAux7M
https://github.com/pfnet/chainer/issues?q=milestone%3Av1.4.0+is%3Aclosed

あまり変更点を追えていないのですが一応メモ。

Restore cuda.init()

Restore cuda.init · Issue #456 · pfnet/chainer · GitHub
1.3.0でcuda.init()が必要なくなった関係で互換性が失われていました。
参考
chainerメモ10 AttributeError: 'module' object has no attribute 'init' - 北の雲のstudylog

1.4.0からは1.3同様に必要は無いけれど、もしcuda.init()があっても互換性維持のためdeprecatedを出すだけでエラーで落ちなくなりました。

CuPy any,all,where,count_nonzero等が追加

これが1.4.0の一番の変更点かも。
CuPyがますますパワーアップしてNumpy互換性が増しました。

Connectionist Temporal Classification(コネクショニスト時系列分類法)

https://github.com/pfnet/chainer/pull/280
凄くかっこいい名前。玄人っぽい。

青い深層本、P126に載っているやつです。
「入出力間で系列長が違う問題を隠れマルコフを使わずに解決する」ものだそうです。
主に音声認識に使われているようです。

イメージとしては、人間が「あいうえお」と発した時に「あ」や「い」の長さは微妙に異なるはず。

  • 入力はこんな感じ

あああ いい うううう えええ おお

  • でも出力はこうしたい

あ い う え お

これを隠れマルコフではなくニューラルネットワークで処理したい場合に使うと解釈しました。
音声以外にも使い道はありそう。

コード
https://github.com/pfnet/chainer/blob/d51c33fc20ff59a25a6b58af29ff022d156cfeb9/chainer/functions/loss/ctc.py
論文
ftp://ftp.idsia.ch/pub/juergen/icml2006.pdf
P44あたりから少し解説
http://ibisml.org/archive/ibis2013/pdfs/ibis2013-kubo.pdf

Softmax cross entropyのignoreフラグ(-1)

https://github.com/pfnet/chainer/pull/500
Softmax cross entropyの正解データに-1を与えると0が返ってくるようになったようです。
バッチ処理したいけど系列長が異なる自然言語で、系列を合わせるために短い文の末尾を-1で埋めちゃえばSoftmax cross entropyに投げてもその部分は0が返ってきてくれるし、計算もしないから処理も速いのかな。
今まではEOSを表すintで埋めてたのでこれは地味に嬉しいかも。

(追記)試してみたのですがGPUだとNANが返ってきちゃいます。これは1.3以前と同じ挙動。CPUだと大丈夫。自分の問題なのかchainerのバグなのか。自分の勘違いでそもそもこういう使い方じゃないのかも。

(2016.1.8 再追記)原因が判明。-1を正解データ(y)じゃなくて学習データ(x)に投げてる箇所があったため。一つずつずらしていくと、どうしてもxに-1が出てきちゃう。
というかEmbedIDに-1を投げても値が返ってくるのは何故なんだろう。ついでにEmbedIDの数を超えててもGPUだとなぜか返ってくる。CPUはちゃんとエラー出てくれる。こっちに詳しく書きました。
studylog.hateblo.jp