人工知能とか犬とか

人工知能と犬に興味があります。しょぼしょぼ更新してゆきます。

最近読んだ論文 2019/11/16

仕事や趣味で読んだ論文について、ごく簡単に紹介していくという記事です。 毎週は難しいかもしれませんが、ちょくちょく書いていきます。

今回は3本の論文を紹介します。

Beyond Word Importance: Contextual Decomposition to Extract Interactions from LSTMs

LSTMに入力されるトークン列の範囲が出力に対してどの程度の影響を持っているのかを分析する手法であるContextual Decompositionを提案しています。

f:id:wanchan-daisuki:20191116105642p:plain
token単位での貢献
"Beyond Word Importance"というタイトルどおり、token列に対するSentiment Analysis について、token単位での出力に対する貢献はもちろん、token列の任意の範囲の貢献も出すことができるという手法です。 いわゆる「説明可能なAI」の流れに属する手法ですが、LIMEのような近似的な線形モデルを構築するのではなく、内部を流れる値を各入力要素の貢献の和に分解することで説明しようという手法です。

Attentionを使った手法などで各tokenが出力(ネガティブ/ポジティブ)にどのように貢献しているのかを可視化する方法はありますが、本手法の特徴は、連続するtoken列の範囲に対して貢献を出力することができます。

f:id:wanchan-daisuki:20191116110755p:plain
連続するtoken範囲の貢献を可視化

手法の肝となるのは、LSTMのセル間を流れる h_t c_tを、token範囲の効果とそれ以外の効果に分離するということです。 f:id:wanchan-daisuki:20191116111853p:plain

こんな分離どうやってやるの、という感じなのですが、LSTMの特性をうまく利用して頑張って分離しているようなのですが、ちょっと読んだ程度ではなかなか理解できません。

また、非線形の変換であるtanhシグモイド関数は、以下のような特性をもつ関数L_{tanh}などを考えることで、各要素の和に対する変換を、各要素に対する返還の和へと分離します。 f:id:wanchan-daisuki:20191116112350p:plain

こんな便利な関数は実際には存在しませんので、L_{tanh}, L_{\sigma}は、入力された値に応じて動的に決まることになります。このような分離は、シャープレイ値の近似であると論文中では説明されています。 f:id:wanchan-daisuki:20191116114134p:plain

公式の実装がgithubで公開されていますが、対応しているPyTorchのバージョンが古く、最新のバージョンで動かすためには、少し変更する必要があります。 github.com

もっとも、本手法の後継論文の実装が2つ、別のリポジトリでActiveになっていますので、ちょっと触ってみてたいということであればこれらのほうが良いでしょう。 前者は、本論文で提案した手法をConv層やReLUなどにも使えるように拡張し、階層的な説明を可能にするというもの。後者は、Contextual Decompositionによって得られた説明に対して、修正を施すことで、モデルを改善できるという手法のようです。

A Topological Loss Function for Deep-Learning based Image Segmentation using Persistent Homology

なんとなくタイトルから、面白そうだと思ったので読んでみた論文です。 本論文は、セグメンテーション結果に対して、パーシステントホモロジーを用いた正則化手法を提案しています。

真のセグメンテーション結果がわからない場合でも、トポロジカルな性質(Betti数)さえわかっていれば、セグメンテーションモデルの出力から計算できる微分可能な値であるPersistence barcodeのbirth/deathを用いて損失を計算できる、ということのようです。

f:id:wanchan-daisuki:20191116132330p:plain

f:id:wanchan-daisuki:20191116132538p:plain
persistence barcodeは、赤が結合部位の出現と消失を、緑が穴の出現とその消失を表す

実用例として、学習済みモデルの出力結果を補正するために使う方法と、半教師あり学習に使う方法が示されています。半教師あり学習に使うケースは、ラベルなしデータに対してトポロジカルな性質が事前にわかっているとし、現実的なユースケースではすべて同一であることが求められるようです。 このようなケースはやや特殊なドメイン(この論文では心臓の写真なので、必ず心室の壁がリング状になる)でのみ有効なようにも思いますが、ドメインによっては強力な正則化や後処理の手段になりそうです。

f:id:wanchan-daisuki:20191116130833p:plain

自分はトポロジーについて超ぼんやりとした理解しかしていないので、これから色々勉強していこうとしているところですが、以下のようなリポジトリ(論文もある)があったりして、道具は徐々に整備されてきているようです。

github.com

SinGAN: Learning a Generative Model from a Single Natural Image

ICCV2019のBest Paperに選ばれた論文です。 単一の画像で作るGANということで、どういうことなんだろうと思って読んでみた論文です。 今週、Qiitaに解説記事を書いてみましたので、詳しくはそちらを御覧ください。

モデル自体はそこまで複雑なところは無いのに、いろいろな応用があるというのが、非常に面白い論文でした。特にアニメーション生成はすごく面白いと思ったので、論文を読んだあとに公式リポジトリのサンプルを動かしてみました。教師データに用意する画像が一枚で済むこともあり、訓練時間は1時間程度ですし、GANを実際に動かしながら学びたいという人にとっての入り口としても良いと思いました。

f:id:wanchan-daisuki:20191116125221g:plain f:id:wanchan-daisuki:20191116125251g:plain

2019年10月の機械学習関連記事まとめ

趣旨

すこし趣向を変えて、ブログを再開してみようと思います。 自分は最近、毎週水曜日にいくつかの情報源をもとにネットを巡回し、面白そうな記事を見つけたときにははてなブックマークでコメントを書くことにしています。

とはいえ、ブックマーク+かんたんなコメント程度では、あまり意味がないと思っていて、定期的に見直す場を作ってみようと思いました。

そういうわけで、毎月頭に、先月自分がつけたブックマークを整理する、という記事を上げていこうと思います。

深層学習手法のデモ

深層学習は、自分で触ってみて「へーこんなことができるんだ」感動すると、俄然興味が湧いてくるものだと思います。

とはいえ、論文を自分で実装するというのは、なかなか骨が折れる。そのため、githubで公開されているソースコードや、デモサイトなんかが用意されていると、たいへん嬉しいものです。

というわけで、公式で用意されているデモを2つと、自分でデモを作るときに役立つかもしれない情報2つを紹介します。

CTRL - A Conditional Transformer Language Model for Controllable Generation

github.com

salesforceのテキスト生成モデルです。訓練するときにジャンルのようなコードを埋め込んで学習することで、それらしい文章が生成できるようになているらしいです。Colabで実行できるデモがあり、実在しないURLからそこに含まれる文字列をヒントに、それっぽい記事を生成する様子が見れます。

colab.research.google.com

10月末に、huggingface/transformersというtransformerベースの手法やpretrainedモデルを集めるコードベースに組み込まれたようです。

FUNITのデモ(GANIMAL)

www.nvidia.com

NVIDIA AI PLAYGROUNDにFUNITで構築したモデルのデモが公開されました。動物の顔画像を生成するGANなので、その名も「GANIMAL」。

f:id:wanchan-daisuki:20191104165452p:plain
FUNITのデモ

現状では、入力画像を選択できるだけなので、FUNITのキモであるFew-shot(変換先の画像が数枚あれば良い)という特性が活かせていないデモになっているのが残念です。

streamlit

github.com MLツールを簡単に作れるという売り文句のライブラリ。創業者がGoogleX出身ということもあり、注目度は高いです。 一通り触ってみて感じたのは、特に凝ったレイアウトが不要であるプロトタイプに最適なライブラリだなあ、という感想です。

個人的にポイントが高いのが、各種可視化ライブラリにデフォルトで対応しており、jupyterで普段から使っている可視化ライブラリのコードを流用できるという点です。あんまり使う機会が無いかもしれませんが、graphviz対応とかも良い感じです。

グラフだけでなく、地図や動画、音声もサクッと埋め込めます。

Python & OpenGL for Scientific Visualization

www.labri.fr

PythonOpenGLを使った科学計算で使う可視化についてのまとめ、というより教科書です。リッチな可視化を作ろうとすると、先程のstreamlitのようなものからスクラッチへの移行を検討する必要があるそうです。

研究・論文

今月面白そうと思った研究をさらっと紹介します。ちゃんと論文を読んだわけではなく、Abstractなどをざっと眺めただけなので、間違っていたらごめんなさい。

Expanding scene and language understanding with large-scale pre-training and a unified architecture - Microsoft Research

www.microsoft.com

テキストとビジョンを統合したBERTのようなUnified Encoder-Decoderの開発。画像中の物体RoI系列とテキスト系列をそのままBERT風のEncoder-Decoderにぶちこんで、マスクされたテキストの穴埋めを予測できるよう事前学習します。事前学習済みモデルをもとに、VQAやImage-Captioningにファインチューニングするらしいです。

Semi-Supervised and Semi-Weakly Supervised ImageNet Models

github.com

教師あり学習と弱教師あり学習を組み合わせて(Semi-Weakly)、10億枚規模のデータセットに対して画像分類モデルを学習するという研究。ここまで大きなデータセットになってくると、自分で手を出すことは無いと思うのだが、中身のロジックくらいは知っておきたいので、後日論文を読む予定(半教師ありと弱教師ありを組み合わせるってどういうこと???)。 torch.hubから学習済みモデルをロードできるようになっています。

Gate Decorator: Global Filter Pruning Method for Accelerating Deep Convolutional Neural Networks

arxiv.org

Andrew NgのThe Batchで紹介されていた、モデルの軽量化手法。各チャネルのスケーリングをするGate機構を導入し、それが0に近いチャネルを削除する、というのが基本的な方針のようです。

ImageNet(ResNet)の計算コストを55%削減・精度は0.67%減、CIFAR(ResNet)の計算コストを70%削減・精度は0.03%増という結果のようです。

facebookresearch/SlowFast

github.com

動画行動検出手法のPythonコードベース。FAIR製。 以下の手法がまとまっているようだ。

  • SlowFast
  • SlowOnly
  • C2D
  • I3D
  • Non-local Network

その他のブックマーク

SpeechBrain: A PyTorch-based Speech Toolkit.

speechbrain.github.io

PyTorchベースの音声認識ツールキットをつくりましょうというプロジェクト。スポンサーにDOLBYがいるのが本気度高そうです。

Hydra

cli.dev facebook製のpython用config管理ツール。少し眺めた程度なので、何ができるのか実はよくわかっていません。うまく使いこなせれば、pythonのargparse周りをきれいに整理できそうな予感があります。 Specializing configurationを見る限り、機械学習のハイパーパラメータをyamlファイルにまとめておく、という使い方もできるみたいです。

BrachioGraph

brachiograph.readthedocs.io

わずか14ユーロ(1800円くらい?)で作るお絵かきマシン。

[https://brachiograph.readthedocs.io/en/latest/images/brachiograph-with-pencil.jpg:image=https://brachiograph.readthedocs.io/en/latest/images/brachiograph-with-pencil.jpg]

この手作り感がたまらない。パーツを集めて作ってみたいですが、そんな暇はあるのでしょうか(汗)

LVISデータセット

www.lvisdataset.org MS-COCOをベースにした、インスタンスセグメンテーションの大規模データセットです。例をみるとわかりますが、かなり細かい部分まで丁寧にアノテーションがなされていることがわかります。

機械学習論文の再現性のためのチェックリスト

Andrew NgのThe Batchで紹介されていた、NeurIPs2019のCall for Papersでも参照されている、機械学習論文の再現性(Reproduction)のためのチェックリストです。

https://www.cs.mcgill.ca/~jpineau/ReproducibilityChecklist.pdf

月に何本か論文を読み、ときに実装する身としては、このくらいの要件はきちんと満たした論文だけになってほしいなあと思う次第です。

できれば、github等で、すべての実験のソースコードを公開してほしいところですが、実験って結構ぐちゃぐちゃになりながらなんとか成果が出るって感じのものがほとんどだと思うので、それをまた整理してgithubに置くのはしんどいのかも、と思ったりもします。

以上

2019年になった。

あけましておめでとうございます。

2019年になったようです。

そういえば最近更新できていないな、と思い出しました。

せっかくなので、今年の目標というか、方向性のようなものをふんわりと書き留めておこうと思います。

このブログは『人工知能とか犬とか』というタイトルのブログなのですが、PyTorchの入門記事ばかりで、犬関係の記事が無いということに気づきました。このままではタイトル詐欺になってしまうので、犬関係の記事を書いていこうと思います。

あと、PyTorchも1.0になって、記事の内容が若干古くなっているので、全体的に更新していこうかなと思います。 PyTorch本体だけではなく、周辺ライブラリにも突っ込んでいこうかなと思っています。

自分は研究者ではなく、ただの趣味でやっているので、色々なジャンルの入門レベルを味見する感じでやっていこうと思います。

そんなわけで、このブログの今後の方針はこんな感じです。

  • 犬系の書籍や論文を読んで、それとなくまとめてみる。
  • PyTorchを中心に色々な分野に(入門程度に)突っ込んでみる。

今年はいい年になるといいなあ(╹◡╹)

PyTorchのDatasetとDataLoader

概要

PyTorchのチュートリアルData Loading and Processing Tutorial をやってみて、DatasetとDataLoaderの使い方を学ぶのです。

DatasetとDataLoader

そもそも、深層学習で用いる教師データは、以下のような処理を必要とします。

  • データの読み込み
  • データの前処理
  • データ拡張
  • データの順番をシャッフルする
  • ミニバッチを作成する
  • シングルマシン or 分散システム中の複数のGPUにミニバッチを配布する

ここらへんの処理を簡単に実現できるのが、DdatasetとDataLoaderです。

Dataset

抽象基底クラスtorch.utils.data.Datasetを継承して、以下のメソッドを実装すれば、独自のDatasetを定義できます。

  • __len__: データセットに含まれる全要素の数を返す。
  • __getitem__: i番目のサンプルをdataset[i]という形で取得できるようにするためのメソッド。
class FaceLandmarksDataset(Dataset):
    """Face Landmarks dataset."""

    def __init__(self, csv_file, root_dir, transform=None):
        self.landmarks_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform

    def __len__(self):
        return len(self.landmarks_frame)

    def __getitem__(self, idx):
        img_name = os.path.join(self.root_dir,
                                self.landmarks_frame.iloc[idx, 0])
        image = io.imread(img_name)
        landmarks = self.landmarks_frame.iloc[idx, 1:].as_matrix()
        landmarks = landmarks.astype('float').reshape(-1, 2)
        sample = {'image': image, 'landmarks': landmarks}

        if self.transform:
            sample = self.transform(sample)

        return sample

これは、以下のようにiterableなオブジェクトとして扱うことができます。

face_dataset = FaceLandmarksDataset(
    csv_file='faces/face_landmarks.csv',
    root_dir='faces/')

for i, sample in enumerate(face_dataset):
    print(sample['image'].shape, sample['landmarks'].shape)
    
    if i > 4:
        break
(324, 215, 3) (68, 2)
(500, 333, 3) (68, 2)
(250, 258, 3) (68, 2)
(434, 290, 3) (68, 2)
(828, 630, 3) (68, 2)
(402, 500, 3) (68, 2)

DataLoader

Datasetは単体では、ただのiterableなオブジェクトに見えますが、DataLoaderと組み合わせることで、最初に挙げた処理を効率的に記述することができます。

まずはバッチサイズが1のミニバッチを生成するDataLoaderを作ってみます。

face_dataset = FaceLandmarksDataset(
    csv_file='faces/face_landmarks.csv',
    root_dir='faces/')
dataloader = DataLoader(face_dataset, batch_size=1,
                        shuffle=True, num_workers=4)

for i_batch, sample_batched in enumerate(dataloader):
    print(i_batch, sample_batched['image'].size(),
          sample_batched['landmarks'].size())
    
    if i_batch > 4:
        break
0 torch.Size([1, 500, 365, 3]) torch.Size([1, 68, 2])
1 torch.Size([1, 333, 500, 3]) torch.Size([1, 68, 2])
2 torch.Size([1, 500, 335, 3]) torch.Size([1, 68, 2])
3 torch.Size([1, 402, 500, 3]) torch.Size([1, 68, 2])
4 torch.Size([1, 334, 500, 3]) torch.Size([1, 68, 2])
5 torch.Size([1, 333, 500, 3]) torch.Size([1, 68, 2])

このように、ミニバッチをDatasetから簡単に作成することができました。上記の例では、imageのサイズが全てバラバラなので、このままでは、複数のサンプルのimageをまとめてミニバッチを作ることはできません。ミニバッチを作るためには、Datasetの__getitem__内でサンプルであるimageのサイズを揃える前処理が必要です。

TransformとCompose

チュートリアルでは、callableなRescaleとRandomCroopというクラスを作成していますが、特にパラメータが必要なければ、sampleを引数として受け取り、変更されたsampleを返す関数でも問題ありません。

def flip_img(sample):
    """
    画像だけを左右反転
    """
    image, landmarks = sample['image'], sample['landmarks']
    image = image[:, ::-1]  # 画像データを左右反転
    return {'image': image, 'landmarks': landmarks}

また、複数のTransformをまとめて、順次処理のパイプラインにすることもできます。 torchvision.transforms.Composeは、以下のように使用できます。

composed = transforms.Compose([
    flip_img,
    Rescale(256),
    RandomCrop(224),
    ToTensor()
])

ComposeをDatasetの__getitem__中で使用することで、ミニバッチ中の画像サイズの大きさを揃えることができます。これでミニバッチにもできますね。

transformed_dataset = FaceLandmarksDataset(
    csv_file='faces/face_landmarks.csv',
    root_dir='faces/',
    transform=transforms.Compose([  # transform引数にcomposeを与える
       Rescale(256),
       RandomCrop(224),
       ToTensor()
    ]))

dataloader = DataLoader(transformed_dataset, batch_size=4,
                        shuffle=True, num_workers=4)

for i_batch, sample_batched in enumerate(dataloader):
    print(i_batch, sample_batched['image'].size(),
          sample_batched['landmarks'].size())

    if i_batch == 3:
        break
0 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
1 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
2 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
3 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])

(おまけ)DataLoaderのcollate_fn

DataLoaderにはcollate_fnという引数を指定できます。実装をみてみると、リストとして処理されるミニバッチ内のサンプルを、最後にTensorに変換する処理を行っていることがわかります。

今ひとつ使い所がわかりませんが、1つのサンプルからデータを抽出してミニバッチを作成するときなどに使うのかもしれません。

まとめ

チュートリアルをもとに、DatasetとDataLoaderの使い方を確認してきました。

DatasetとDataLoaderは適切に使えれば、データの前処理をきれいに書くことができる便利なクラスです。Datasetは、最初にデータを一括して読み込んでインメモリで処理したいとか、毎回ディスクから画像を読み込んでデータ拡張をしたいといった様々なケースに対応可能なようになっています。

効率的な処理ときれいなコードのために、習得しておきたいところです。

PyTorchの分散環境学習

概要

PyTorchのチュートリアルに、分散環境での学習に関する記事がある。 自分の家のサーバーはシングルGPUなのだが、最近少々不満を感じてきている。

クラウドでの分散学習を見越して、今後のために勉強しておくことにしたのです。

なお、特に実装はありません。

分散学習の構成要素

torch.distributedによって、マルチプロセスやクラスターでの実行、要は並列化を用意におこなえるようになります。 以下ではその構成要素を整理しています。

バックエンド

PyTorchでは、torch.multiprocessingという並列処理の機能がありますが、これは単一のマシン内での並列化にとどまります。一方で、torch.distributedは、以下のような並列処理用バックエンドを使った大規模クラスタでの並列処理をサポートします。

  • TCP:CPUしかサポートしていない。初期実装向け?
  • Gloo:CPUとGPU共にサポート。多分一番メジャー?
  • NCCL:Gloo同様だが、このチュートリアル作成時に追加されたらしい。まだ詳しいことはわからないが、NVIDIA謹製だけあって、GPU間のやり取りは高速な模様。
  • MPI:CPUとGPU共にサポートしているが、PyTorchのバイナリファイルにMPI用の実装が含まれていないので、自分で再コンパイルする必要があるらしい。

process, rank, group, world, size

用語の整理。

  • process:分散環境で動作する個々のプロセス。
  • rank:個々のprocessを指示するインデックス。Masterのprocessは0で固定みたい。
  • group:Collectiveを行う範囲を指定するprocessのサブセット。
  • world:全process。
  • size:groupやworldに含まれるprocessの数。

通信方法

Point-to-Point

process間でTensorをやり取りをする方法です。

send/recv

dist.senddist.recvによって、ノード間でTensorを送受信します。

isend/irecv

dist.isenddist.irecvによって、ノード間で非同期に、Tensorを送受信します。受信内容の反映は、非同期の送受信が完了するのをwait()してあげて、初めて保証されます。

Collective

複数のノード間でTensorを配布したり集計したりする方法です。

  • dist.scatter(tensor, src, scatter_list, group)srcのprocessからgroup内の各processにscatter_listで指定したTensorを送付し、各processのtensorに格納する。
  • dist.gather(tensor, dst, gather_list, group)group内の各processからtensorを集め、dstprocessのgather_listに格納する。
  • dist.all_gather(tensor_list, tensor, group)group内の各processからtensorを集め、group内の全processのtensor_listに格納する。
  • dist.broadcast(tensor, src, group)group内のsrcprocessのtensorを各processのtensorに格納する。
  • dist.reduce(tensor, dst, op, group)group内の各processのtesnsorを集めて、opで指定された処理を行い、dstprocessのtensorに格納する。
  • dist.all_reduce(tensor, op, group)group内の各processのtensorを集めて、opで指定された処理を行い、各processのtensorに格納する。

  • dist.barrier(group):groupに含まれるprocessがこのポイントに至るまで待機する。

Operation

dist.reducedist.all_reduceで指定するop、つまりOperationには、以下の4つがあります。すべて要素ごとに計算は行われます。

  • dist.reduce_op.SUMtensorの要素ごとの和
  • dist.reduce_op.PRODUCTtensorの要素ごとの積
  • dist.reduce_op.MAXtensorの要素ごとの最大値
  • dist.reduce_op.MINtensorの要素ごとの最小値

共有ファイルシステム

group内のprocess間で、同一のファイルを参照したり上書きしたりしながら処理を進めることがあります。競合が発生しないために、fcntlによるロックをサポートしているファイルシステムを採用する必要があるそうです。普通の分散ファイルシステムなら普通にサポートしていそうですが、どうなんでしょうかね?

まとめ

PyTorch TutorialのWriting Distributed Applications with PyTorchを読んで、まとめてみました。

クラウド上のリソースを使った分散学習で少しでも学習時間が減らせればと思って手を出そうとしているのですが、趣味でやっているので、お財布と要相談ですね…。

PyTorchのSeq2Seqをミニバッチ化するときに気をつけたこと

概要

PyTorchチュートリアルに、英仏の機械翻訳モデルを作成するTranslation with a Sequence to Sequence Network and Attentionがあります。 このチュートリアルは、教師データを一つずつ与える形になっており、結構遅いのです。 なので、バッチでの学習ができるように修正を試みたところ、注意ポイントがいくつかあったのでまとめておきます。

RNNのバッチ学習の実装

RNNでバッチ学習を行う際に問題となるのが、入力されるデータ系列の長さがバッチ内で異なることです。 この問題には一般的に、バッチ内での長さを揃えるためのパディングと、パディングした部分が学習の邪魔にならないようにするマスキングを実装して対処する必要があります。

実装自体は割と簡単にできますが、きちんと実装しないと学習が全然進まなかったりするので注意が必要です。

パディング

パディング自体はそう難しい処理ではありませんが、ググったりフォーラムを参照したり、調べ始めるといろいろやり方があって混乱してしまいました。結果として2つに落ち着きました。

  • 元データに対して、パディングトークン(今回は0)を必要な長さになるまで追加する。
  • 元データを個別にTensorにし、pad_sequenceを使う。

大規模な学習を行う必要のない場合は、前者で良さそうです。

1つ目のやり方

こちらは説明の必要も無いくらい簡単にできます。 単純に足りない長さ分のパディングトークンを追加するだけです。

max_length = 10
seq = [1, 1, 1, 1, 1]
seq += [0] * (max_length - len(seq))
seq
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]

悩むのは、max_lengthをバッチ内での最大系列長にするか、グローバルな定数にしておくかぐらいでしょうか。

2つ目のやり方

こちらはpad_sequenceを使う方法ですが、シーケンスの長さが降順になるようにソートする必要があり、少々めんどくさいです。

a = torch.ones(2)
b = torch.ones(3)
c = torch.ones(4)
d = torch.ones(3)
e = torch.ones(1)

# 長さが降順になるようにソート
sorted_tensors = sorted([a, b, c, d, e], key=lambda x: x.shape[0], reverse=True)

# Padding
nn.utils.rnn.pad_sequence(sorted_tensors, batch_first=True)
tensor([[ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  0.],
        [ 1.,  1.,  1.,  0.],
        [ 1.,  1.,  0.,  0.],
        [ 1.,  0.,  0.,  0.]])

pad_sequenceは、すでにTensorとして用意されているデータをリストに格納し、パディング処理するという用途には向いていそうですが、これを単体で使うことはあまり無い気がしています。

公式ドキュメントのFAQにある、My recurrent network doesn’t work with data parallelismを読む限りでは、複数GPUや分散環境での学習時に必要な、PackedSequenceという仕組みを使うときに使うようです。単一の環境で行う場合は、無理に使う必要も無いでしょう。

マスキング

マスキングは、以下の2点が適切に行われる必要があるでしょう。

  • Embedding
  • Loss

Embedding

Embeddingでは、padding_idx引数を指定する必要があります。 これを指定することで、パディングされた部分の埋め込みをすべて0にすることができます。

num_input = 10
emb_size = 5

embedding = nn.Embedding(num_input, emb_size, padding_idx=0)
batch = torch.LongTensor([
    [1, 2, 3, 0, 0, 0, 0, 0, 0, 0],
    [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
])

embedding(batch)
tensor([[[ 0.6500,  0.1616, -1.1696, -0.0516, -0.9050],
         [-0.4270,  1.1525, -0.8994, -1.0899, -0.6576],
         [ 0.4006,  0.3189,  0.1728,  1.4344,  2.0811],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000]],

        [[ 0.6500,  0.1616, -1.1696, -0.0516, -0.9050],
         [-0.4270,  1.1525, -0.8994, -1.0899, -0.6576],
         [ 0.4006,  0.3189,  0.1728,  1.4344,  2.0811],
         [ 0.6779, -0.7535,  0.1944,  0.8275, -0.5984],
         [ 0.9328, -1.4141,  1.0738,  1.5253, -1.1572],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000]]])

Loss

Seq2Seqをミニバッチで行う場合、損失の計算を行う際にパディング部分を適切に処理しないと、損失の計算結果がで大きく変わってしまいます。これによって、損失を過大評価したり過小評価したりといったことが生じてしまうので、パディング部分をマスクして損失は計算する必要があります。

loss1 = nn.NLLLoss()
loss2 = nn.NLLLoss(ignore_index=0)  # ignore_indexを指定

pred = torch.rand(100, 5)
true = torch.randint(high=5, size=(100,), dtype=torch.long)

print(loss1(pred, true))
print(loss1(pred[true > 0], true[true > 0]))
print(loss2(pred, true))
tensor(-0.4701)
tensor(-0.4473)
tensor(-0.4473)

パディング部分をマスクした損失は、上記の例だとloss1(pred[true > 0], true[true > 0])で計算することも可能ですが、loss2のようにignore_indexを指定することでも実現できます。面倒なので、ignore_indexを指定したほうが良いでしょう。

その他

PyTorchに限らず、深層学習の実装を行う際は、層に期待されている入出力がどういうサイズのテンソルなのかを適切に把握しておく必要があります。PyTorchのドキュメントでは、引数にテンソルを取る場合のサイズや順序を明確に示しているので、ドキュメントをよく読みましょう。

僕が実装の過程で躓いたのは以下の2つくらいでした。

  • RNNのbatch_first引数:デフォルトでは入力も出力も(seq_len, batch, input_size)というサイズのTensorだが、これをTrueにすると(batch, seq_len, input_size)になる。
  • 内積計算:バッチに含まれているTensor同士の内積の計算は、torch.bmmで実現できる。 torch.transposeやsqueeze、unsqeezeなどと組み合わせてサイズの順番を適切にしてから計算する必要がある。

まとめ

PyTorchのチュートリアルも、Attention機構になってくると複雑になってきます。 パディングとマスキング周りは結構調べながら実装しました。テンソルのサイズは最初は混乱しますが、丁寧に一つ一つの処理を追い、transposeやsqueeze, unsqeezeを駆使しながら実装するのは、パズルのようで楽しい作業でもあります。

一応、今回実装したものをNotebookにしてあります。Decoderは、チュートリアルとは別のバージョンを一つ実装しています。Attentionっていろいろ種類がありますね。 いずれもろくにチューニングしていないので、精度的に良いという感じではありません。残念ながら、Attentionの結果も納得の行くものにはなっていません。

PyTorchにはfairseqのようなパッケージもあるので、こういうものを活用して、Seq2Seqは手軽に実装できるようになっておきたいです。

Word2Vecと多義性

概要

Word2Vecは、単語をベクトルとして表現する手法ですが、「ダウンタウン」のような語は、多義性を持っています。 事実、word2vecにおける「ダウンタウン」は「ウッチャンナンチャン」のようなお笑い芸人コンビよりも、「シーサイド」などの地理的な用語とのほうが類似度が高くなることがあります。 SVDを使って、ベクトル表現された「ダウンタウン」からのお笑い芸人要素を抽出してみたいのです。

準備

ダウンタウン」の多義性

ダウンタウンに類似している単語を、学習済みword2vecモデルを使って調べてみましょう。

from gensim.models.word2vec import Word2Vec
model_path = 'word2vec.gensim.model'
model = Word2Vec.load(model_path)

target_word = "ダウンタウン"
model.wv.most_similar(target_word, topn=10)

きっとこんな出力が得られるはずです。

[('ミッドタウン', 0.8537717461585999),
 ('中心街', 0.8283530473709106),
 ('アップタウン', 0.8175398111343384),
 ('ストリート', 0.8096662759780884),
 ('マンハッタン', 0.8070244193077087),
 ('セントラル・パーク', 0.7880061864852905),
 ('チャイナタウン', 0.7842386364936829),
 ('セントラルパーク', 0.7788770198822021),
 ('メインストリート', 0.772649884223938),
 ('リバーサイド', 0.7724398374557495)]

都市や町を表すような横文字の単語がたくさん出てきます。ダウンタウンはお笑いコンビであると同時に、「繁華街」を表す単語でもあります。 ちなみに、自分がぱっと思いつくお笑い芸人がどこに出てくるかというと、、、

owarai_words = ['タモリ', 'さんま', '明石家さんま', 'ビートたけし', 'サンドウィッチマン',
                'ウッチャンナンチャン', 'とんねるず', 'ナインティナイン', 'バナナマン']

for i, (word, score) in enumerate(model.wv.most_similar("ダウンタウン", topn=100000)):
    if word in owarai_words:
        print(i+1, word, score)
7737 ウッチャンナンチャン 0.37757110595703125
9201 とんねるず 0.36377257108688354
15844 ナインティナイン 0.3186175227165222
16347 サンドウィッチマン 0.31598663330078125
21048 バナナマン 0.29402637481689453
22260 タモリ 0.28919389843940735
27073 明石家さんま 0.27145278453826904
34131 ビートたけし 0.2494104504585266
45667 さんま 0.2202756255865097

なんと7737番目に「ウッチャンナンチャン」が出てきます。類似度も0.38と、そんなに高くありません。 この学習済みモデル内では、ダウンタウンはお笑い芸人というよりは、「繁華街」の意味が強いみたいです。

多義性を抽出する

ダウンタウンのベクトル表現の中に含まれているであろうお笑い芸人要素をなんとかして抽出してみましょう。以下のような戦略を取ります。

  1. とりあえず類似している単語を検索してそこそこの数を集める
  2. あつめた類似語の中で支配的なベクトル成分をSVDによって特定する
  3. SVDで得られた変換によって、元のword vectorから、支配的な成分を差し引く
  4. 差し引いたベクトルで検索をかけてみる

類似している単語を集める

とりあえず30単語くらい集めてみましょう。

model.wv.most_similar(positive=[target_word], topn=30)
[('ミッドタウン', 0.8537717461585999),
 ('中心街', 0.8283530473709106),
 ('アップタウン', 0.8175398111343384),
 ('ストリート', 0.8096662759780884),
 ('マンハッタン', 0.8070244193077087),
 ('セントラル・パーク', 0.7880061864852905),
 ('チャイナタウン', 0.7842386364936829),
 ('セントラルパーク', 0.7788770198822021),
 ('メインストリート', 0.772649884223938),
 ('リバーサイド', 0.7724398374557495),
 ('ベイエリア', 0.7706623077392578),
 ('ロウアー・マンハッタン', 0.7596601247787476),
 ('シュガーランド', 0.7591447234153748),
 ('アヴェニュー', 0.7567015290260315),
 ('ナッシュビル', 0.7514258623123169),
 ('アッパー・イースト・サイド', 0.7497652769088745),
 ('ヴィレッジ', 0.7473465204238892),
 ('タイムズスクエア', 0.7472085952758789),
 ('マンハッタン区', 0.7465125918388367),
 ('スコッツデール', 0.7446001172065735),
 ('大通り', 0.7414740920066833),
 ('ハイウェイ', 0.7391560077667236),
 ('ハイツ', 0.7379716634750366),
 ('ビーチ', 0.7375644445419312),
 ('イースト', 0.7367238998413086),
 ('タウン', 0.7340238094329834),
 ('メイン・ストリート', 0.7318717241287231),
 ('セントポール', 0.7312244176864624),
 ('アベニュー', 0.7290509939193726),
 ('ビレッジ', 0.727695643901825)]

トップ30の単語に含まれる強い成分を抽出してみましょう。

from sklearn.decomposition import TruncatedSVD
similar_words = [x[0] for x in model.wv.most_similar(positive=[target_word], topn=30)]
x = np.array([model.wv[target_word]] + [model.wv[word] for word in similar_words])

# SVDで最も強い成分を取り出す
svd = TruncatedSVD(n_components=1)
transformed_x = svd.fit_transform(x)
extracted_x = svd.inverse_transform(transformed_x)

# プロットしてみる
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)
fig.set_figheight(4)
fig.set_figwidth(16)
ax1.bar(np.linspace(0, 49, 50), x[0])
ax2.bar(np.linspace(0, 49, 50), extracted_x[0])
ax3.bar(np.linspace(0, 49, 50), x[0] - extracted_x[0])

ax1.set_ylim(-0.4, 0.4)
ax2.set_ylim(-0.4, 0.4)
ax3.set_ylim(-0.4, 0.4)

ax1.set_title("ダウンタウン")
ax2.set_title("強い成分")
ax3.set_title("残りの部分")

f:id:wanchan-daisuki:20180617220242p:plain

もともとの「ダウンタウンのベクトル」から「強い成分」を引いて、検索してみましょう。 gensimでは、most_similarメソッドの引数のnegativeに差し引きたい成分を入れることでやりたいことが実現できます。

model.wv.most_similar(positive=[target_word], negative=[extracted_x[0]], topn=30)
[('毎回', 0.6686477661132812),
 ('トーク', 0.6482667922973633),
 ('バラエティ', 0.6234453916549683),
 ('ギャグ', 0.6228033304214478),
 ('番組内容', 0.6177021265029907),
 ('視聴者', 0.6139947175979614),
 ('リスナー', 0.6131852269172668),
 ('笑い', 0.6000913977622986),
 ('ネタ', 0.5957863330841064),
 ('出演者', 0.5891598463058472),
 ('下ネタ', 0.5873293280601501),
 ('人気番組', 0.5856291651725769),
 ('とんねるず', 0.5822530388832092),
 ('面白い', 0.581822395324707),
 ('コント', 0.5800341367721558),
 ('タモリ', 0.575900137424469),
 ('お笑い', 0.5756425857543945),
 ('楽しく', 0.574203610420227),
 ('本音', 0.5713927745819092),
 ('お色気', 0.5688245892524719),
 ('芸人', 0.5651636719703674),
 ('お笑い番組', 0.5635770559310913),
 ('ナレーション', 0.5626435279846191),
 ('喋り', 0.5624589323997498),
 ('聴取者', 0.5620508790016174),
 ('ファン', 0.561782956123352),
 ('深夜番組', 0.5606791377067566),
 ('フリートーク', 0.5589464902877808),
 ('雰囲気', 0.5542364120483398),
 ('トーク番組', 0.5526514053344727)]

お笑いや芸能関係の単語がたくさん出てきましたね。お笑い芸人の名前もちらほら見えます。

繰り返し抽出する

以下のような手順を何度か繰り返すことで、「ダウンタウン」に関わる多義性を炙り出せるのではなかろうか、と考えられます。

  1. negativeリストを用意する
  2. 単語とnegativeリストで検索する
  3. トップn個の類似単語から、強い成分を抽出する
  4. 得られた強い成分で検索し、類似している語を得る
  5. 強い成分をnegativeリストに加え、2.に戻る
from ipywidgets import interact

def extract_similar_words(word, n_iter=10):
    wordvec = model.wv[word]
    negative = []
    
    vectors = [wordvec]
    top_words = [[(word, 1.0)],]
    
    for i in range(n_iter):
        # topnを変えることで、少し結果が変わります。
        similar_words = [x[0] for x in model.wv.most_similar(positive=[word], negative=negative, topn=10)]  # 類似した単語を集める
        x = np.array([wordvec] + [model.wv[word] for word in similar_words])  # 特異値分解の行列を作成する
        svd = TruncatedSVD(n_components=1)
        transformed_x = svd.fit_transform(x)
        extracted_x = svd.inverse_transform(transformed_x)
        
        vectors.append(extracted_x[0])
        top_words.append(model.wv.most_similar(positive=[extracted_x[0]], topn=10)) 
        
        negative.append(extracted_x[0])  # negativeな成分を追加
        
    # interactiveなwidgetを使っています。
    @interact(factor=(0, n_iter))
    def wordvec_plot(factor=0):
        x = np.arange(50)
        plt.bar(x,vectors[factor])
        plt.title("{}つ目の成分".format(factor))
        plt.ylim((-0.5, 0.5))
        plt.text(60, -0.4, '\n'.join(["{}: {}".format(x[0], x[1]) for x in top_words[factor]]))
        
    return wordvec_plot
extract_similar_words("ダウンタウン")

1つ目の成分で検索してみると、街区や地区に関する、主に横文字が出てきます。 f:id:wanchan-daisuki:20180617220646p:plain

2つ目の成分で検索してみると、お笑い芸人が出てきます。 f:id:wanchan-daisuki:20180617220650p:plain

3つ目の成分で検索してみると、よくわからない結果になります。残されたベクトルの成分も平坦になってきているので、多義性を抽出し尽くした、と言えるのかもしれません。 f:id:wanchan-daisuki:20180617220654p:plain

ウマやイヌで試す

ダウンタウン」には、はっきりと違う2つの意味=多義性がありました。 しかし、例えば「馬」は人間と様々かたちで関わる動物です。 それぞれの観点によって類似している単語は異なってくるはずです。

  • 哺乳類としての馬 → 様々な哺乳類
  • 食べ物としての馬 → 様々な食用動物
  • 役畜としての馬 → 牛
  • 乗り物としての馬 → 車、自転車
  • 競争するものとしての馬 → フォーミュラカー
  • 戦争の道具としての馬 → 戦車

などということが想像できます。

ウマ

extract_similar_words("ウマ")

1つ目の成分で検索してみると、草食動物や家畜っぽい動物が出てきます。 f:id:wanchan-daisuki:20180617222913p:plain

2つ目の成分で検索してみると、競馬っぽい単語が出てきます。 f:id:wanchan-daisuki:20180617222555p:plain

ちなみに、「ウマ」ではなく「馬」で検索してみると、当然ですが、全く違う結果が得られます。

1つ目の成分では、競馬系の単語が並びます。 f:id:wanchan-daisuki:20180618212030p:plain

2つ目の成分で検索してみると、乗り物としての馬や戦(いくさ)を意識させるような単語が出てきましたね。 f:id:wanchan-daisuki:20180618212037p:plain

イヌ

ついでに犬でもやってみましょう。

extract_similar_words("イヌ")

1つ目の成分で検索してみると、動物や昆虫が出てきました。「イヌ」自身も類似後のトップ10に出現しています。 f:id:wanchan-daisuki:20180618212236p:plain

2つ目の成分で検索してみると、、、なぜか法律や病気に関する単語が出てきます。1つ目の成分の類似語に「イヌ」そのものが含まれていたことを考えると、1つ目の成分で「イヌ」の意味はほとんど説明できている、と言えるのかもしれません。 f:id:wanchan-daisuki:20180618212242p:plain

まとめ

Word2Vecで得られた埋め込みベクトルを分析し、単語の中に含まれている多義性ごとに類似単語を得ることができることを確認しました。 また、現実において多様な扱われ方をする「馬」にも、多義性のようなものが含まれていることがわかりました。

Word2Vecを含めたEmbeddingは様々な自然言語処理の深層学習に必須のテクニックですが、単体で見ても分析しがいのある題材ですね。