某大学生の機械学習日記

趣味はでぃーぷらーにんぐ

Ubuntu16.04LTSのGPU用環境構築

家のパソコンのOSを再インストールし、環境構築を1からやったのでその備忘録を。。。

スペック
- GeForce1080
- Ubuntu16.04LTS

目次
- 初期エラー
- nvidiaドライバーのインストール
- CUDA9.1のインストール
- cuDNNv7.0のインストール
- python3.6のインストール


* 初期エラー

初期時に発見したエラーがいくつかあったので記載する。
1. ネットワーク回線が頻繁に切れる
これは、調べたところDNSの場所が設定できていなことが原因でおきるエラーで'/etc/network/interfaces'に

dns-nameservers 8.8.8.8

と追記したことで解決した。



* nvidiaドライバーのインストール

まず、デフォルトで入っているドライバーのnouveauを無効化する。
’/etc/modprobe.d/blacklist-nouveau.conf’ファイルを新しく作り、そこに

blacklist nouveau
options nouveau modeset=0

と書き込むその後、'sudo update-initramfs -u'をうち無効化させる。 nvidiaの公式サイトから自分のgpuに合うドライバーのversionを確認し以下によりインストールを行う。

sudo add-apt-repository ppa:xorg-edgers/ppa
sudo apt update
sudo apt install nvidia-384

これで'reboot'し、'nvidia-smi"を打つことでドライバーが認識されていることが確認された。



* CUDA9.1のインストール

CUDAの最新は9.2だがPyTorchの公式サイトを見ると9.1までしかサポートされていない(?)とのことだったので、CUDA9.1をインストールをする。
公式サイトからdeb(network)をダウンロード。

sudo dpkg -i cuda-repo-ubuntu1604_9.1.*_amd64.deb
sudo apt-key adv --fetch-keys http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/7fa2af80.pub
sudo apt update
sudo apt install cuda-9-1

によりCUDA9.1のインストールは完了。PATHの設定も必要なので

echo -e "\n## CUDA and cuDNN paths"  >> ~/.bashrc
echo 'export PATH=/usr/local/cuda-9.1/bin:${PATH}' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-9.1/lib64:${LD_LIBRARY_PATH}' >> ~/.bashrc
source ~/.bashrc

とターミナル上で行う。



* cuDNNv7.0のインストール

nvidia-developersの'~for cuda9.1'のところから以下の三つのファイルをダウンロードしてくる。 「cuDNN v7.0.5 Runtime Library for Ubuntu16.04 (Deb), cuDNN v7.0.5 Developer Library for Ubuntu16.04 (Deb), cuDNN v7.0.5 Code Samples and User Guide for Ubuntu16.04 (Deb)」そして、

sudo dpkg -i libcudnn7_7.0*+cuda9.1_amd64.deb
sudo dpkg -i libcudnn7-dev_7.0*+cuda9.1_amd64.deb
sudo dpkg -i libcudnn7-doc_7.0*+cuda9.1_amd64.deb 

により展開する。これでcuDNNのインストールは完了。



* python3.6のインストール

Ubuntu16.04にはデフォルトでpython3.5が入っているが、基本的にpython3.6で開発を行うのでインストールを行う。

# Install python3.6
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update
sudo apt-get install python3.6 python3.6-dev

# Install pip3
wget http://bootstrap.pypa.io/get-pip.py
sudo python3.6 get-pip.py

これで'python3.6'と打つことで起動する。しかしこのままではpython3で読み出されるのがpython3.5なので

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.5 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 2

これによってデフォルトでpython3.6が起動するようにした。ちなみに、

sudo update-alternatives --config python3

このコマンドを打つことによっていつでもpython3のPATHを入れ替えることができる。

janome+LDAでトピックモデル

動機

  • ニュース記事等のトピック推定については腐る程で出回ってるからちょっと違った切り口からトピックモデルを実践したかった

  • "janome"というめちゃくちゃ日本語前処理の便利なツールを見つけたから使ってみたかった

コーパス

名大会話コーパスを用いた

前処理

  • 入力文を分かち書きにする
    janome.tokenizer.Tokenizer()を用いる。引数でmmap=TrueとすることでNEologd辞書となる。

  • ユニコードの正規化
    "janome.charfilter.UnicodeNormalizeCharFilter"

  • 文字種の統一(数字→0など)、記号の排除
    NumericReplaceFilter() ← 漢数字を0にする
    RegexReplaceCharFilter('\d+', '0') ← 数字を0にする
    RegexReplaceCharFilter(u'>', '') ← 「>」を排除
    ...

  • 英文字を全て小文字化
    LowerCaseFilter()

  • 名詞のみを抽出する
    POSKeepFilter(['名詞'])

  • 漢数字を0にする
    janomeの組み込み関数にはないので自分で定義する必要がある

class NumericReplaceFilter(TokenFilter):

    def apply(self, tokens):
        for token in tokens:
            parts = token.part_of_speech.split(',')
            if (parts[0] == '名詞' and parts[1] == '数'):
                token.surface = '0'
                token.base_form = '0'
                token.reading = 'ゼロ'
                token.phonetic = 'ゼロ'
            yield token
  • 1文字のものを排除する
    OneCharacterReplaceFilter()で読み出すが、これもjanomeの組み込みにはなかったので定義する必要があった。
class OneCharacterReplaceFilter(TokenFilter):
    def apply(self, tokens):
        for token in tokens:
            if len(token)==1:
                continue
            yield token

上記の処理関数を統合してjanome.analyzerに渡して前処理機は完成だ。あと個人的にストップワードをいくつか省きたかったからそれを省くコードも下には含まれている。これだけのコードで基本的な前処理が行えてしかも追加のフィルターも簡単に定義できてしまうのがjanomeの魅力だと感じた。

def janome_meishi(sentence):

    # janomeモデルの定義
    token_filters = [
                     NumericReplaceFilter(), # 漢数字を0にする
                     POSKeepFilter(['名詞']),  # 名詞のみを抽出する
                     LowerCaseFilter(),  # 英字は小文字にする
                     ExtractAttributeFilter('base_form'),# 原型のみを取得する
                     OneCharacterReplaceFilter() # 1文字のものを除外する
                     ]
    char_filters = [UnicodeNormalizeCharFilter(), 
                    RegexReplaceCharFilter('\d+', '0'),
                    ...
                    RegexReplaceCharFilter(u'Inf', u''),
                    RegexReplaceCharFilter(u'>', u'')
                    ]
    tokenizer = Tokenizer(mmap=True) 

    analyzer = Analyzer(char_filters, tokenizer, token_filters)
    text = [w for w in analyzer.analyze(sentence)]

LDA

BoWの作成
出現回数が1回以下のものと0.4割以上の文書で現れる単語は省いたBoWを作成した

dictionary = gensim.corpora.Dictionary(text_data)
    dictionary.filter_extremes(no_below=1, no_above=0.4)

LDAの学習

corpus = [dictionary.doc2bow(words) for words in text_data]

lda = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                          num_topics=2,
                                          id2word=dictionary,
                                          random_state=19)

AKRacingのゲーミングチェアが届いたぞー!!(おまけでNLPについてのぼやき)

ちょーかっこいい。。。
作業の時間がかなり増えることを見越してそれなりの椅子をって探して即決したこの椅子、かっこいいわ座り心地も最高だわ捗ること間違いなし。

f:id:knto-h:20180524003411j:plain:w400

っていうだけだと機械学習日記にならないので一応最近思ったことを。

自然言語の文脈

 最近は僕の空き時間の7割型を自然言語周りの技術のリサーチに使っているのだが、調べていても目的によって様々な方法が採られており、人間の知識を可能な限りモデルに組み込もうという工夫を随所に感じた。そのため、調べれば調べるほど奥(闇)の深さが見えてくるようになりやべー知らないことばっかだーってなってるのが現状(強い人の肩にのりたい)。

 あと面白いなと思ったのは、文書要約を重要文抽出問題と捉えることで検索アルゴリズムを適用できるようにしたということとか(PageRank, TextRank)。課題を別の視点から観察して解決を試みる柔らかい発想は見習わないとなと感じた。

  • 抽出系
     個人開発レベルで色々(doc2vec/word2vec, SVM, RNN系, tfidf,,,)を使って簡単に意味のある語の抽出を試みたものの、単語抽出に関していえば従来の機械学習モデルの方がまだ精度良く、通常のDLだと難しいのかなと言った感触だった。それに一貫して言えるのがもっと前処理やハイパラの調整を丁寧に正確に行わないと自然言語処理では実践レベルの精度は出せないだろうなという感想だった。

  • 生成系
     僕が今関心を寄せているのはこの分野。ちゃんとした文章が生成できるという点については懐疑派なので、実際に手を動かしてみてどの程度のものが出来上がるのは感触を確かめたい。     つい最近googleがメールタイピングで文章予測をオンタイムで行う技術を発表していたがそれを見た限りでは文章としてはまともだった。まあでも日本語と英語じゃ勝手が違うだろうし、日本語じゃそんなうまくいかないだろうなと思っている。個人的にbest practiceな日本語の生成法は、Google APIを用いてまず英語に翻訳して英語で処理を施して結果を得る方法。日本語→英語の翻訳はかなり精度いいのでちゃんとしたものはできるはずだ。

今やってる強化学習の勉強がひと段落ついたらなんかここ関連で作ってみたいな。でもただ生成モデルを作るだけだと既存のパクリにしかならないし、どうせなら何か(画像/音声とか)と絡めてやってみたいな。   

(ほのぼの報告日記でした^^)

PyTorchにおける並列化処理について~備忘録~

動機
  • cpuの並列処理+GPUの並列処理が必要なモデルを実装する上で知識の整理をしておきたかったから

  • 時短という意味でもこれから使いたいから

  • 知らないことが多そうで単純に面白そうだったから

CPUでの処理並列化

(参照: Multiprocessing best practices — PyTorch master documentation)

import torch.multiprocessing as mp

num_processes = 4
model = MyModel()
model.share_memory()
processes = []
for rank in range(num_processes):
    p = mp.Process(target=train, args=(model,))
    p.start()
    processes.append(p)
for p in processes:
   p.join()

1~2行目でプロセス数の指定model()の読み込みを行なっている。
3行目でモデルのパラメータを共有メモリ上に移動させている。
5~8行目でそれぞれのプロセスが異なる処理を行う。
9~10行でそれぞれのプロセスの終了。
この並列化を適用したものが、"hogwild(SGDの並列化計算)"でpytorch/example/にコードが上がっているので参照するとイメージがつかめるはず。
自分のパソコンで作成中の強化学習モデルのある部分を切り出して、CPU並列化を適用した時としなかった時で比較して見た。 結果:20s(並列化なし), 31s x 2 (並列化有り、プロセス数:2)

GPUでの並列処理

(参照: Multi-GPU examples — PyTorch Tutorials 0.4.0 documentation)

import torch
mynet = torch.nn.DataParallel(MyModel, device_ids=[0, 1, 2])

 "nn.DataParallel(module, device_ids=None)"とすることで指定した複数gpuで(defaultでは全GPU)バッチ処理の並列処理をおこなう。そのため、指定するdevice数はバッチサイズよりは小さくないといけなく、理想としてはbatch sizeがdevice数の定数倍。
 似たようなものとして"nn.parallel.DistributedDataParallel(module, device_ids=None)"があるが、これを使うには制約が多そうなので上のnn.DataParallelを使うのが無難そう。

CPU+GPU

(Module内でCPU処理とGPU処理を混在させたとき)
(参照: Multi-GPU examples — PyTorch Tutorials 0.4.0 documentation)

import torch
import torch.nn as nn

device = torch.device("cuda:0")

class DistributedModel(nn.Module):

    def __init__(self):
        super().__init__(
            embedding=nn.Embedding(1000, 10),
            rnn=nn.Linear(10, 10).to(device),
        )

    def forward(self, x):
        # Compute embedding on CPU
        x = self.embedding(x)

        # Transfer to GPU
        x = x.to(device)

        # Compute RNN on GPU
        x = self.rnn(x)
        return x

 このクラスを呼び出した時、embedding()の部分はCPUで計算、それ以降はGPUにデータを移動させてGPUで計算をおこなう。このmodelに対してnn.DataParallel()を適用すると、gpu部分のみ並列化される(はず?)。

DNNアンサンブル学習手法"Stochastic Weight Averaging(SWA)"で遊んでみた

動機

  • 最近学校の課題が忙しく、ガッツリ系の実装ができていない中ちょうど良さげなものが見つかったから。

  • これ本当か?と感覚的には理解できなかったので実際に試してみて調べたかったから

Stochastic Weight Averaging(SWA)の概要

従来のアンサンブル学習ではモデル・アンサンブルだったのに対して、SWAではウェイト・アンサンブルをとった。つまり、複数のモデルの重みの平均を取り込んだモデルで結果を予測する。感覚的に俄には信じられないが、Resnet150大型のモデルで効力を発揮しているようだ。

従来のニュールモデルのアンサンブル学習の二つの手法
  1. Snapshot Ensembling
    Learning Rate Schedulingのサイクルが終わるごとにその時のモデルの"weight"を保存する。そのサイクルをいくつか繰り返し、保存したweightをもつモデルのアンサンブルによって推定を行う手法。
  2. Fast Geometric Ensembling (FGE) Snapshot Ensemblingと似ているが、異なる点が二つある。一つ目は、サイクルの周期が数エポックごとにしていて計算時間の短縮をした。二つ目に"inear piecewise cyclical learning rate schedule"を起用している(Snapshotではcosine)。ちょっとした違いだが、Snapshotのより精度が高くなったらしい。
SWA

前述の手法の欠点はweightを保つ必要があったためメモリを食ってしまうところ。SWAではモデル二つぶんのweightsのみでアンサンブル学習()を実現し、かつ前述の良いところを引き継いだ構造となっている。
二のつモデルの役割:
* 一つ目のモデルは、cyclical learning scheduleに基づいてベストなweightを探索するもの(式中のw)
* 二つめのモデルは、1サイクルごとに求められたweightを蓄えるモデル(式中のw_swa)
以下の式が具体的な更新方法 f:id:knto-h:20180516131052p:plain:w400

実装

モデルの定義
今回は小さいデータセットでどんな挙動を示すかが気になったので、fashion-mnistを用いることにした。それにさいしモデルを以下のように(半ば適当に)定義。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1,16,kernel_size=4,stride=2) # 12x12
        self.bn1 = nn.BatchNorm2d(16)
        self.conv2 = nn.Conv2d(16,32,kernel_size=4, stride=2) # 5x5
        self.bn2 = nn.BatchNorm2d(32)
        self.fc1 = nn.Linear(32*5*5, 120)
        self.fc2 = nn.Linear(120,84)
        self.fc3 = nn.Linear(84,10)

    def forward(self, x):
        B,_,_,_ = x.size()
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = x.view(B, -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        out = self.fc3(x)
        return out

SWA関数の定義
二つのモデル(model_swa, model)のパラメータ処理を定義。

def SWA(model_swa, model, n):
    _n = n
    for w_swa, w in zip(model_swa.parameters(), model.parameters()):
        w_swa.data *= _n/(_n+1)
        w_swa.data += w.data/(_n+1)

メイン関数とハイパーパラメータ
main()にてループを回す。一回のサイクルあたり4回エポックを回し、それを10回繰り返した。つまり、10回model_swaのパラメータを更新した。

def main():
    model = Net().to(device)
    model_swa = Net().to(device)

    ...
    ...

    epochs = 4
    cycles = 10
    loss_data = []
    for cycle in range(cycles):
        print("-------  Cycle {0}  -------".format(cycle+1))
        optimizer = optim.SGD(model.parameters(), lr=0.1)
        scheduler = optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.1)
        for epuch in range(epochs):
            scheduler.step()
            train(model, criterion,optimizer,train_loader)
            average_accu = validate(model, criterion, val_loader)
        SWA(model_swa,model, cycle)
        validate(model_swa, criterion, val_loader)

結果

  • 普通のモデルでの精度は大体99.8%(train), 99.1%(test)ぐらいをうろちょろしてた。

  • SWAモデルではサイクルごとに[86.41, 85.39, 84.58, 83.52, 83.77, 84.09, 84.24, 84.62, 84.52, 84.58]とかだった。。。

  • あまりに大雑把にやりすぎたせいか結果がでなかったから時間があるときにもっと研究してみたいなー。

参考

原論文 → https://arxiv.org/pdf/1803.05407.pdf
記事 → Stochastic Weight Averaging — a New Way to Get State of the Art Results in Deep Learning

Parallel WaveNetの論文を読み解く

原論文→https://arxiv.org/pdf/1711.10433.pdf

Parallel WaveNetとは
簡単にいうならば、WaveNetよりも早く音声合成を行えるようにしたモデル。WaveNetではoutput音素を次の音素を出すためのinputとする回帰な接続をもつため音声一つ出すだけでも時間がかかるという問題点があった。それを解決するべくでてきたのがParallel-WaveNet。同時分布的な入力を使って出力を一発で出せるということから音声合成にかかった時間を短縮させた。個人的には、時系列的な入力を必要とした従来のWaveNetを使って、同時分布的な入力をつかうstudentモデルを学習させるための工夫のところが面白かった。蒸留(distillation)と呼ぶらしい。

学習
teacherモデルとstudentモデル(←学習させたいモデル)をつかう。論文ではteacherモデルに学習済みWaveNetを用いている。WaveNet teacherは学習するのが早く、結果を得るのが遅い。それに対してWaveNet student は学習が遅く、結果を得るのが早いというのを逆手にとった構造となっている。下の図がとてもわかりやすい。Studentモデルのinput(s)にはランダムに生成した白色雑音を使っている。tにおけるoutput(s)では時系列に依存しないようになっているため、output(s)は並列的に得ることができる。そして、そのoutput(s)をteacherモデルのinput(t)として利用する。もともとWaveNetは回帰的な構造を持っているためWaveNet studentのoutput(s)をinput(t)としても問題はない。このようにWaveNet studentをWaveNet teacherでtrainする手法を論文では"distillation(蒸留)"というらしい。
f:id:knto-h:20180501012950p:plain:w400

Loss計算
通常のDNNのトレインでは出力のL1平均、L2平均をLossとするのをよく散見する。しかし、今回の場合はモデルをロジスティック分布の繰り返し(論文では"a mixture o logistics distribution"と読んでいた)で確率密度関数とみなしているため、WaveNet teacherとWaveNet studentそれぞれとのKL距離(Kullback-Leibler divergence)をLossとして学習させている。下の式がそれを表している。交差エントロピーの公式から導出可能である。確率密度関数としているのでそれぞれの出力は確率であらわされている。
f:id:knto-h:20180501114240p:plain:w400
f:id:knto-h:20180501121826p:plain:w200
f:id:knto-h:20180501121851p:plain:w200

Lossの追加

  • Power loss
    このLoss関数を入れることによってhigh-entropy状態(例えば囁きのよう)になることを防ぎ、より人の発言に似せることを目指す。
    f:id:knto-h:20180501170815p:plain:w200
    φ: SIFT(Short-Term-Fourier Transform)の二乗, g: WaveNet studentによる変換, (y,c): 教師データを表している。yは実際の音声でcはextra-conditioning-data(話者の性質を表すものだと僕は理解している)

  • Perceptual loss
    悪い発音になることを防ぐ目的でloss関数を定義した。WaveNet studentによって合成された音声と実際の音声との差をlossとしている。論文ではstyle lossを用いることで精度を改善した。

  • Contrastive loss
    同じ話者同士のKL-divergence Lossを小さくし、違う話者とのKL-divergence Lossを大きくするために導入されたLoss関数 f:id:knto-h:20180501172340p:plain:w200

深層強化学習"Ape-X"の論文を読みとく

Ape-XとはICLR2018で提案された深層強化学習アルゴリズム。既存モデルを大きく上回る成績&学習速度の改善をえた。 (実装もしているので、それの経過等も別の記事で報告できたらと思います)(時間があればまた更新します...)

元論文→Distributed Prioritized Experience Replay

論文のまとめ

ベースモデル

  • Actor-Learnerをベースとしている
    ルートモデルのLearnerを効率的に学習させていくための工夫(Distributed SGD, Distributed Importance Sampling, Prioritized Experience Replay)がなされている。工夫の詳細は後のパートにて説明する。大まかな流れとしてはLearnerからクローンされた複数のActorは非同期的に方策評価をおこない観測経験をReplay Memoryに保存する。そして、Replay Memoryから呼び出された経験をもとにLearnerのパラメーターを更新する。 f:id:knto-h:20180419021823p:plain:w450

工夫点

  • Distributed Stochastic Gradient Descent
    まずLearnerからクローンされた複数のActorが非同期的に経験をサンプリングしてくる。そしてActorによって蓄積されたReplay Memoryの中からランダムに情報をサンプリングしLearnerの更新に回すことからきている。この2つのステップに加えて、ActorのサンプリングはCPUをLearnerの学習ではGPUで計算することによって学習の高速化をはかる。

  • Distributed Importance Sampling
    Replay Memoryから優先度を反映した確率分布に基づいてサンプリングをおこない、パラメーターの更新をする。 勾配のばらつきを減らすことで効率的かつより早く学習がおこなえる。

  • Prioritized Experience Replay
    通常のExperience ReplayではReplay Memoryの中からランダムにサンプリングしてきたミニバッチを用いてパラメーターの更新をする。しかしこれでは学習に優位なデータがサンプリングされにくかったりその逆のことがおき非効率である。そこで、TD誤差によって経験に優先度をつけて保存し重要な経験が呼び出されやすくなるよう効率的な学習を行う手法を起用した。優先度をだすことから"Prioritized"がきている。

f:id:knto-h:20180419022733p:plain:w400

モデル構造(詳細)

  • Loss関数
    Mean Square rootを用いる。Double-DQNの手法を使って, targetをLearnerと別のモデルから求める。