{"cells":[{"metadata":{},"cell_type":"markdown","source":"# 概要\n\nこんな人向け：コンペ初心者。何したらいいかわからない人。\n\n自分の備忘録も兼ねているので間違っていたら教えてください。"},{"metadata":{},"cell_type":"markdown","source":"# 1. LOADING\n\nまずはデータを読み込みましょう。"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"numpy：各種計算に使用\n\npandas：表計算やデータのロードに使用\n\n両方ともほぼ必ず使用するのでとりあえずインポートしてもいいくらいです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"train = pd.read_csv(\"../input/bms-molecular-translation/train_labels.csv\")\nprint(train.shape)\ntrain.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"pandasの.read_csvでファイルを読み込みます。\n\n右端の|<というメニューからパスをコピーできます。\n\n242万行のデータですね。\n\nimage_idは画像データのＩＤでInChIが今回予測したい文字列です。"},{"metadata":{},"cell_type":"markdown","source":"# 2. IMAGE"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(\"../input/bms-molecular-translation/train/0/0/0/000011a64c74.png\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"画像はこんな感じでimage_idの1,2,3文字目でフォルダ分けされ、最後にimage_id.pngの名前で保存されています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import cv2","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"cv2は画像処理によく使われます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"train[\"file_path\"] = train[\"image_id\"].apply(lambda x: f\"../input/bms-molecular-translation/train/{x[0]}/{x[1]}/{x[2]}/{x}.png\")\nprint(train[\"file_path\"].values[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"image_idを.applyで処理していきます。１文字目がx[0],２文字目がx[1]...となりパスが作成できます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train[\"file_path\"].values[0]\nimage = cv2.imread(path)\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"cv2.imreadで指定したパスの画像を読み込みました。\n\nこの画像は229x325で赤緑青３色です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import matplotlib.pyplot as plt\nplt.style.use(\"seaborn-white\")\nplt.imshow(image)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"matplotlibはグラフ作成によく使われるライブラリですが、画像表示にも利用できます。\n\n有機物の構造式ですね。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(train[\"InChI\"].values[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"これが上図構造式のInChI表示です。画像からInChIを予測するのが今回のタスクです。"},{"metadata":{},"cell_type":"markdown","source":"# 3. VOCABULARY\n\n文字を予測しなければならないので、どんな文字があるのか整理します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"words = set()\nfor s in train[\"InChI\"]:\n    words.update(set(s))\nprint(words)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"set()はユニークなデータを取り出すことができます。\n\nデータのInChIすべてに対してユニークな文字を取り出し、wordsを更新しましょう。\n\n全行の処理が終わったころにはすべてのInChIに含まれる文字がwordsに入っています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"vocab = list(words)\nprint(vocab)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"このvocabリストにある文字を使えばデータ内にあるすべてのInChIを表現できます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"vocab.append(\"<sos>\")\nvocab.append(\"<eos>\")\nvocab.append(\"<pad>\")\nprint(vocab)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ここで３つの文字を追加しましょう。\n\nsos：文字の始まり。\n\neos：文字の終わり。\n\npad：パディング。データサイズの調整。\n\n後で役に立ちます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"VOCAB_SIZE = len(vocab)\nprint(VOCAB_SIZE)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"これで文字を整理する一連の作業は終わり。合計で41種の文字を利用します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"stoi = {\n    'C': 0,')': 1,'P': 2,'l': 3,'=': 4,'3': 5,\n    'N': 6,'I': 7,'2': 8,'6': 9,'H': 10,\n    '4': 11,'F': 12,'0': 13,'1': 14,'-': 15,\n    'O': 16,'8': 17,',': 18,'B': 19,'(': 20,\n    '7': 21,'r': 22,'/': 23,'m': 24,'c': 25,\n    's': 26,'h': 27,'i': 28,'t': 29,'T': 30,\n    'n': 31,'5': 32,'+': 33,'b': 34,'9': 35,\n    'D': 36,'S': 37,'<sos>': 38,'<eos>': 39,'<pad>': 40\n}\n\nitos = {item[1]:item[0] for item in stoi.items()}","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"機械学習に文字を直接ぶち込むことはできません。\n\nなので文字を数値に変換して学習と予測を実行し、最後にまた文字に戻す作業が必要です。\n\nstoiはstring to index(indices)でもじを数値に変えます。itosは逆。"},{"metadata":{"trusted":true},"cell_type":"code","source":"inchi = train[\"InChI\"].values[0]\nprint(inchi)\ninchi = [stoi[s] for s in inchi]\nprint(inchi)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"このように文字を数値のリストに変換できます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"train[\"length\"] = train[\"InChI\"].apply(lambda x: len(x))\nMAX_LEN = train[\"length\"].max()\nprint(MAX_LEN)\ntrain.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"次に最大文字数を取得しておきましょう。これも後々必要です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train.loc[train[\"length\"] == MAX_LEN][\"file_path\"].values[0]\nplt.imshow(cv2.imread(path))\nplt.show()\nprint(train.loc[train[\"length\"] == MAX_LEN][\"InChI\"].values[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ちなみに最長の構造はこれです。こんなもの予測できたら苦労しませんね。"},{"metadata":{},"cell_type":"markdown","source":"# 4. DATASET\n\n今回モデル作成にはpytorchを使います。\n\npytorchではモデルに読み込ませるデータを自作することが多いです。\n\nとりあえず最低限必要なデータは画像とInChIです。\n\nまずはどんな手順でデータを作成するか確認しましょう。"},{"metadata":{},"cell_type":"markdown","source":"## ◆◇IMAGE◇◆\n画像はさっきと同じ要領"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train[\"file_path\"].values[0]\nimage = cv2.imread(path)\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"さっきと同じ。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\nplt.imshow(image)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"cv2で画像を読み込むとBGRという色の順になります。\n\nこれをRGBに直すために.COLOR_BGR2RGBに通しました。\n\nこれが学習に効くのかは知りません。私は気分だと思っています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train[\"file_path\"].values[1]\nimage = cv2.imread(path)\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ここで気を付けるべきは画像サイズ。\n\n１枚目は229x325でしたが２枚目は148x288です。\n\n画像サイズを統一しないとエラーになるので処理しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import albumentations as A","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"albumentationsは画像加工に便利なライブラリです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train[\"file_path\"].values[0]\nimage = cv2.imread(path)\nimage = A.Resize(256, 256)(image = image)\nprint(image)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":".Resizeで256x256に変換しました。\n\n注意点は辞書型で返ってくることです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image = image[\"image\"]\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"変換されていますね。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image = A.Normalize()(image = image)[\"image\"]\nprint(image)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"次は正規化処理です。これをしておくと学習が早く収束する？みたいです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from albumentations.pytorch import ToTensorV2\nimage = ToTensorV2()(image = image)[\"image\"]\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"albumentations.pytorch.ToTensorV2でtorch型に変換しましょう。\n\npytorchでは色を表すデータを１個目に使うので3x256x256となっています。\n\n画像処理はこんな感じです。\n\n他にもalbumentationsで左右反転や回転などさせたりしますが、割愛。"},{"metadata":{},"cell_type":"markdown","source":"## ◆◇InChI◇◆\n次は予測したいInChIの加工方法です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"inchi = train[\"InChI\"].values[0]\nprint(inchi)\ninchi = [stoi[s] for s in inchi]\nprint(inchi)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"これでリスト化までできました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"inchi.insert(0, stoi[\"<sos>\"])\ninchi.append(stoi[\"<eos>\"])\nprint(inchi)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"文字の始まりsosと終わりeosを足しておきましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch\ninchi = torch.LongTensor(inchi)\nprint(inchi)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最後にpytorchの.LongTensorでpytoch用のデータに変換します。\n\nこれで終わり。"},{"metadata":{},"cell_type":"markdown","source":"## ◆◇DATASET◇◆\nこれまでの処理をまとめてデータセットにします。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Datasetでセットを作り、DataLoaderで読み込みます。\n\nこいつらはほぼ必須なので覚えておきましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"class TrainDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n        self.paths = self.df[\"file_path\"].values\n        self.inchi = self.df[\"InChI\"].values\n        \n    def __len__(self):\n        return self.df.shape[0]\n    \n    def __getitem__(self, idx):\n        path = self.paths[idx]\n        image = cv2.imread(path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image = A.Compose([\n            A.Resize(256, 256),\n            A.Normalize(),\n            ToTensorV2()\n        ])(image = image)[\"image\"]\n        inchi = self.inchi[idx]\n        inchi = [stoi[s] for s in inchi]\n        inchi.insert(0, stoi[\"<sos>\"])\n        inchi.append(stoi[\"<eos>\"])\n        inchi = torch.LongTensor(inchi)\n        return image, inchi","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データセットはクラスで作ります。\n\ninit：初期化条件。dfは後々渡すデータになります。\n\nlen：データサイズを定義するためのもの。大抵はデータの行数にします。\n\ngetitem：実際にデータを呼び出すための関数。引数にはインデックスが使われます。\n\n最初はよくわからないかもしれませんが３回くらい自作してみると理解できます。\n\ngetitemではさっき書いた処理を全て行いましょう。\n\nidxでインデックスが渡されるので、パスやInChIを読み取って変換していきます。\n\nalbumentationsの.Composeを使えば一連の処理を実行してくれるので便利です。\n\n最後にreturnで画像とinchiを出しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"ds = TrainDataset(train)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"実際にデータセットを作りました。\n\ninitで定義した引数を渡します。今回はデータフレームのみ。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(ds[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"１個目のデータをとりだしました。\n\n画像データとInChIが出力されます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(len(ds[0]))\nprint(ds[0][0].shape, ds[0][1].shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"サイズもあってますね。"},{"metadata":{"trusted":true},"cell_type":"code","source":"for i in range(5):\n    print(ds[i][0].shape, ds[i][1].shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ここでネックなのがInChIの文字数が統一されていないことです。\n\nこれからDataLoaderでバッチ(まとまり)ごとに取り出す変換を行います。\n\nその際に画像と同じくInChIのサイズもバッチ内で統一しないといけません。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.nn.utils.rnn import pad_sequence\n\ndef bms_collate(batch):\n    images, labels = [], []\n    for data in batch:\n        images.append(data[0])\n        labels.append(data[1])\n    labels = pad_sequence(labels, batch_first = True, padding_value = stoi[\"<pad>\"])\n    return torch.stack(images), labels","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"toch.nn.utils.rnnにpad_sequenceという関数があります。\n\nこれはtensor型のデータをパディングしサイズを統一してくれます。\n\nパディングにつかう文字は\"pad\"です。ここで役に立ってくれるわけです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"loader = DataLoader(ds, batch_size = 8, collate_fn = bms_collate)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"DataLoaderにデータセットとバッチサイズ(１度に何個取り出すか)、さっきの変換関数を渡します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"batch = next(iter(loader))\nprint(batch[0].shape, batch[1].shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"next(iter())で次のバッチを取り出せます。バッチサイズを見てみると８になっていますね。"},{"metadata":{"trusted":true},"cell_type":"code","source":"for i in range(8):\n    print(batch[1][i])\n    print(\"=\" * 100)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"InChIを確かめましょう。\n\n全ての文字が\"sos\"の38からスタートしてeosの39で終わり、残り文字数は\"pad\"の40で調整されています。\n\n１個だけ39で終わっているデータがあるので、そいつの文字数に合わせてパディングされているわけです。"},{"metadata":{},"cell_type":"markdown","source":"# 5. CV\nデータ分割はめんどくさいのでtrain_test_splitにします。\n\n本来はもっと正確な分割をすべきです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"DEBUG = True\nif DEBUG:\n    df = train.sort_values(by = \"length\").reset_index(drop = True).copy()\n    df = df.iloc[:1000, :]\nelse:\n    df = train.copy()\nprint(df.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"上手くモデルが作成できているか確認するだけならデータサイズを落としましょう。\n\n簡単にしたいのでInChIの文字数が少ないデータにしました。\n\n実際に提出する際はDEBUGをFalseにしますが、１回の学習に６時間以上かかるので学習コストはかなり厳しいです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn.model_selection import train_test_split\ndf_train, df_valid = train_test_split(df, test_size = 0.1, shuffle = True)\nprint(df_train.shape, df_valid.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データ分割しました。学習が900行で評価が100行です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from tqdm.notebook import tqdm\n\ntrain_data = TrainDataset(df_train)\nvalid_data = TrainDataset(df_valid)\ntrain_loader = DataLoader(train_data, batch_size = 64, shuffle = True, drop_last = True, collate_fn = bms_collate, num_workers = 4)\nvalid_loader = DataLoader(valid_data, batch_size = 64 * 2, shuffle = False, drop_last = False, collate_fn = bms_collate, num_workers = 4)\n\nfor batch in tqdm(train_loader):\n    pass\nfor batch in tqdm(valid_loader):\n    pass","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"先程と同じ要領でデータセットとデータローダーを作ります。\n\nshuffleはデータシャッフル。drop_lastはバッチで切ったデータの最後の端数を切るかどうか。\n\nnum_workersは並行処理数です。後でわかりますが計算量が膨大なのでGPUを使うことになります。その為に処理数を増やしておきます。\n\ntqdmを使うと進捗がわかりやすいので便利です。とりあえずpassで全データを読み込めているか確認しました。"},{"metadata":{},"cell_type":"markdown","source":"# 6. MODEL"},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch.nn as nn\nimport torchvision\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(DEVICE)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"torch.nnとtorchvisionをインポートします。\n\nDEVICEはCPUorGPUを定義します。\n\nGPUがONになっていると.cuda.is_availableはTrueです。"},{"metadata":{},"cell_type":"markdown","source":"## ◆◇Encoder◇◆\n\n今回画像データから特徴量を作成するモデルをEncoderとします。"},{"metadata":{"trusted":true},"cell_type":"code","source":"resnet = torchvision.models.resnet18()\nfor params in resnet.state_dict():\n    print(params)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"torchvisionからresnet18を取り出しました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"modules = list(resnet.children())[:-2]\nresnet = nn.Sequential(*modules)\nfor params in resnet.state_dict():\n    print(params)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最後のfc層は不要なので消しておきましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"batch = next(iter(train_loader))\nimage = batch[0][0]\nprint(image.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"試しに１つ画像を通します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoded = resnet(image.unsqueeze(0))\nprint(encoded.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"変換後のサイズは512x8x8です。\n\nこんな感じでresnetを使って画像の特徴をとらえたデータを作成しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"class Encoder(nn.Module):\n    def __init__(self):\n        super().__init__()\n        resnet = torchvision.models.resnet18()\n        modules = list(resnet.children())[:-2]\n        self.resnet = nn.Sequential(*modules)\n        \n    def forward(self, images):\n        batch_size = images.size(0)\n        features = self.resnet(images)\n        features = features.permute(0, 2, 3, 1)\n        features = features.view(features.size(0), -1, features.size(-1))\n        return features\n    \nencoder = Encoder()\nencoded = encoder(batch[0])\nprint(encoded.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"クラスでEncoderを作りました。\n\ninitではresnetを定義します。\n\nforwardでは実際に行われる処理を記述します。今回は画像データをresnetにとおして出力しましょう。\n\n.permuteはデータの行列(位置)を変える関数です。512を最後に持っていきます。\n\n次元数が多いと不便なので.viewで調整しています。-1の部分は任意の数になるので8x8になります。"},{"metadata":{},"cell_type":"markdown","source":"## ◆◇Decoder◇◆\n\nDecoderはEncoderでの変換後の画像データとInChIを学習させるモデルです。\n\nLSTMを使うと時系列に対応した学習が可能です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"inchi = batch[1]\nprint(inchi.shape)\nprint(inchi)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"InChIはこのようにインデックスのリストにしました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"embedding = nn.Embedding(num_embeddings = VOCAB_SIZE, embedding_dim = 256)\nembeds = embedding(inchi)\nprint(embeds.shape)\nprint(embeds)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Embeddingは文字を数値ベクトルに変換します。\n\n引数は語彙数(今回はVOCAB_SIZE)と変換後の次元数（適当）です。存在を忘れていたVOCAB_SIZEがここで使われます。\n\n変換後のサイズを見ると256にデータが拡張されています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(encoded.shape)\nencoded = encoded.mean(dim = 1)\nprint(encoded.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"次に画像データの特徴を使いますが、次元数が多いのでdim = 1において平均をとりました。\n\n平均にした深い意味はありません。サイズ調整のためだけです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(inchi[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"これからLSTMで時系列データを学習させます。\n\n例えば１個目のInChIは38, 7, 31...と続いていますが、最初の38と画像の特徴から次の７を予測したいわけです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(embeds[:, 0].shape)\nprint(encoded.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"１文字目はembedsの１列目に該当します。\n\nそして画像の特徴はEncoderで変換して.meanでサイズ調整したデータです。\n\nこれらから２文字目の７を学習させます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"lstm = nn.LSTMCell(input_size = (256 + 512), hidden_size = 512, bias = True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"LSTMCellはLSTMと呼ばれる時系列データが学習可能な層です。\n\n詳細は詳しく語れないので調べてください。\n\ninput_sizeに入力するサイズを指定します。今回は１つの文字をEmbeddingで変換した256と画像の特徴である512を足したサイズになります。\n\nLSTMに渡すデータは１つ前の文字と画像の特徴、そして１つ前のLSTMが出力した戻り値(h, c)です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"init_h = nn.Linear(in_features = 512, out_features = 512)(encoded)\ninit_c = nn.Linear(in_features = 512, out_features = 512)(encoded)\nprint(init_h.shape, init_c.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最初の文字は前の文字のデータがないので全結合層でh,cを作ります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"lstm_input = torch.cat((embeds[:, 0], encoded), dim = 1)\nprint(lstm_input.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"文字データと画像の特徴量は.catでくっつけました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"h, c = lstm(lstm_input, (init_h, init_c))\nprint(h.shape, c.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"これらをLSTMに渡しましょう。出力されるのは予測した文字に当たるhと記憶状態を表すCです。\n\n意味不明かもしれませんが、私は「へー」くらいの理解にしています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"lstm_input = torch.cat((embeds[:, 1], encoded), dim = 1)\nh, c = lstm(lstm_input, (h, c))\nprint(h.shape, c.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"３文字目は２文字目とさっきの戻り値h,cとで入力します。\n\nこれを全文字数繰り返すことでInChIを学習していくわけです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"class Decoder(nn.Module):\n    def __init__(self, vocab_size, embed_size, encoder_dim, decoder_dim):\n        super().__init__()\n        self.vocab_size = vocab_size\n        self.decoder_dim = decoder_dim\n        self.embedding = nn.Embedding(vocab_size, embed_size)\n        self.init_h = nn.Linear(in_features = encoder_dim, out_features = decoder_dim)\n        self.init_c = nn.Linear(in_features = encoder_dim, out_features = decoder_dim)\n        self.lstm = nn.LSTMCell(input_size = (embed_size + encoder_dim), hidden_size = decoder_dim, bias = True)\n        self.drop = nn.Dropout(p = 0.3)\n        self.linear = nn.Linear(in_features = decoder_dim, out_features = vocab_size)\n        \n    def forward(self, features, inchis): #渡されるのは画像特徴量と実際のInChI\n        embeds = self.embedding(inchis)\n        \n        features = features.mean(dim = 1)\n        h = self.init_h(features) #最初は画像だけからh,cを作る\n        c = self.init_c(features)\n        \n        seq_length = len(inchis[0]) - 1 #最後の文字になると終わりなので１だけ引く\n        batch_size = inchis.size(0)\n        preds = torch.zeros(batch_size, seq_length, self.vocab_size).to(DEVICE) #予測した文字を格納するために作成\n        \n        for s in range(seq_length):\n            lstm_input = torch.cat((embeds[:, s], features), dim = 1) #s文字目と画像特徴量をくっつける\n            h, c = self.lstm(lstm_input, (h, c)) #hが予測されたデータ\n            x = self.drop(h)\n            x = self.linear(x)\n            preds[:, s] = x #s文字目の次の文字の予測\n        return preds","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Decoderをクラスで作成しました。\n\nやってることはこれまでの解説と同じです。\n\n最後に予測したpredsを吐き出したいのでバッチサイズと文字数、VOCAB_SIZEで枠を作りましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(batch[0].shape, batch[1].shape)\nencoded = encoder(batch[0])\nprint(encoded.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"振り出しに戻りますがバッチデータの画像は3x256x256でInChIが54各文字です。\n\nEncoderで画像から特徴量を取り出します。\n\nこれらがDecoderの入力となります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"decoder = Decoder(vocab_size = VOCAB_SIZE, embed_size = 256, encoder_dim = 512, decoder_dim = 512)\npreds = decoder(encoded, batch[1])\nprint(preds.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Decoderに入れるとバッチサイズ x 文字数-1 x 語彙数で出力されました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(preds[0].shape)\nprint(preds[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"こんな感じで各文字に対して各語彙の数値が格納されています。\n\n41文字の中からもっとも値の大きい文字を予測した文字としましょう。"},{"metadata":{},"cell_type":"markdown","source":"# 7. TRAINING"},{"metadata":{"trusted":true},"cell_type":"code","source":"if DEBUG:\n    BATCH_SIZE = 16\n    EPOCHS = 5\nelse:\n    BATCH_SIZE = 64\n    EPOCHS = 1","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_loader = DataLoader(train_data, batch_size = BATCH_SIZE, shuffle = True, drop_last = True, collate_fn = bms_collate, num_workers = 4)\nvalid_loader = DataLoader(valid_data, batch_size = BATCH_SIZE * 2, shuffle = False, drop_last = False, collate_fn = bms_collate, num_workers = 4)\n\nencoder = Encoder().to(DEVICE)\ndecoder = Decoder(vocab_size = VOCAB_SIZE, embed_size = 256, encoder_dim = 512, decoder_dim = 512).to(DEVICE)\n\nencoder_optimizer = torch.optim.Adam( encoder.parameters(), lr = 1e-4, weight_decay = 1e-6, amsgrad = False)\ndecoder_optimizer = torch.optim.Adam( decoder.parameters(), lr = 1e-4, weight_decay = 1e-6, amsgrad = False)\n\nencoder_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( encoder_optimizer, T_max = 4, eta_min = 1e-4, last_epoch = -1 )\ndecoder_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( decoder_optimizer, T_max = 4, eta_min = 1e-4, last_epoch = -1 )\n\ncriterion = nn.CrossEntropyLoss(ignore_index = stoi[\"<pad>\"])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"optimizer：最適化手法。とりあえずAdam\n\nscheduler：学習率(lr)の調整。色々あるけどみんな使ってるCosineAnnealingLR。\n\ncriterion：損失関数。CrossEntropyLossはsoftmaxに変換してから誤差を計算してくれます。\"pad\"は学習に関係ないので除外しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoder.train()\ndecoder.train()\nfor images, inchis in tqdm(train_loader):\n    images = images.to(DEVICE)\n    inchis = inchis.to(DEVICE)\n    encoder_optimizer.zero_grad()\n    decoder_optimizer.zero_grad()\n    encoded = encoder(images)\n    preds = decoder(encoded, inchis)\n    print(preds.shape, inchis.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"学習させるときは.train()で学習モードにします。何をかえているかは知りません。\n\n各変換を経てpredsを出力しましょう。\n\nここでサイズを見てみるとpredsは53文字に対して実際のInChIは\"sos\"があるので54文字です。\n\nなのでlossが計算できません。"},{"metadata":{"trusted":true},"cell_type":"code","source":"loss = criterion(preds.permute(0, 2, 1), inchis[:, 1:])\nprint(loss.item())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"InChIは\"sos\"がいらないので１以降にしましょう。\n\nまたpredsは現在の順番ではlossが計算できないので入れ替えました。（ココはあってるか怪しいです。。。）"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoder.train()\ndecoder.train()\nfor images, inchis in tqdm(train_loader):\n    images = images.to(DEVICE)\n    inchis = inchis.to(DEVICE)\n    encoder_optimizer.zero_grad()\n    decoder_optimizer.zero_grad()\n    encoded = encoder(images)\n    preds = decoder(encoded, inchis)\n    loss = criterion(preds.permute(0, 2, 1), inchis[:, 1:])\n    print(loss.item())\n    loss.backward()\n    encoder_optimizer.step()\n    decoder_optimizer.step()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"lossを計算できたらloss.backwardとoptimizer.stepで更新しましょう。\n\nこの辺りはpytorchの基礎を検索するとわかりやすい記事があります。\n\n以上で１回の学習が終わりました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoder.eval()\ndecoder.eval()\nvalid_loss = 0\nfor images, inchis in tqdm(valid_loader):\n    images = images.to(DEVICE)\n    inchis = inchis.to(DEVICE)\n    with torch.no_grad():\n        encoded = encoder(images)\n        preds = decoder(encoded, inchis)\n        loss = criterion(preds.permute(0, 2, 1), inchis[:, 1:])\n        print(loss.item())\n        valid_loss += loss.item()\nvalid_loss /= len(valid_loader)\nprint(\"mean loss : \", valid_loss)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"評価用データのvalidでlossを計算しましょう。これが実際に予測したときに近い誤差です。\n\nこのlossが小さければ小さいほど（今回のデータ分割において）良いモデルです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"best_loss = np.inf\nfor epoch in range(EPOCHS):\n    encoder.train()\n    decoder.train()\n    for images, inchis in tqdm(train_loader):\n        images = images.to(DEVICE)\n        inchis = inchis.to(DEVICE)\n        encoder_optimizer.zero_grad()\n        decoder_optimizer.zero_grad()\n        encoded = encoder(images)\n        preds = decoder(encoded, inchis)\n        loss = criterion(preds.permute(0, 2, 1), inchis[:, 1:])\n        loss.backward()\n        encoder_optimizer.step()\n        decoder_optimizer.step()\n    encoder.eval()\n    decoder.eval()\n    valid_loss = 0\n    for images, inchis in tqdm(valid_loader):\n        images = images.to(DEVICE)\n        inchis = inchis.to(DEVICE)\n        with torch.no_grad():\n            encoded = encoder(images)\n            preds = decoder(encoded, inchis)\n            loss = criterion(preds.permute(0, 2, 1), inchis[:, 1:])\n            valid_loss += loss.item()\n    valid_loss /= len(valid_loader)\n    print(f\"[epoch{epoch}] loss:{valid_loss}\")\n    if valid_loss < best_loss:\n        best_loss = valid_loss\n        torch.save(encoder.state_dict(), \"bms_encoder.pth\")\n        torch.save(decoder.state_dict(), \"bms_decoder.pth\")\n        print(\"saved...\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"EPOCHの数だけ学習を繰り返しました。\n\n学習を重ねる度にlossが減りますが必ず減るわけではありません。\n\nなのでbest_lossを無限大で定義して更新していき、最もlossの小さいモデルを保存しましょう。"},{"metadata":{},"cell_type":"markdown","source":"# 8. INFERENCE\n最後に画像データのみから文字を作成しなければなりません。\n\nそのためにDecoderにpredict関数を足します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"class Decoder(nn.Module):\n    def __init__(self, vocab_size, embed_size, encoder_dim, decoder_dim):\n        super().__init__()\n        self.vocab_size = vocab_size\n        self.decoder_dim = decoder_dim\n        self.embedding = nn.Embedding(vocab_size, embed_size)\n        self.init_h = nn.Linear(in_features = encoder_dim, out_features = decoder_dim)\n        self.init_c = nn.Linear(in_features = encoder_dim, out_features = decoder_dim)\n        self.lstm = nn.LSTMCell(input_size = (embed_size + encoder_dim), hidden_size = decoder_dim, bias = True)\n        self.drop = nn.Dropout(p = 0.3)\n        self.linear = nn.Linear(in_features = decoder_dim, out_features = vocab_size)\n        \n    def forward(self, features, inchis): #渡されるのは画像特徴量と実際のInChI\n        embeds = self.embedding(inchis)\n        \n        features = features.mean(dim = 1)\n        h = self.init_h(features) #最初は画像だけからh,cを作る\n        c = self.init_c(features)\n        \n        seq_length = len(inchis[0]) - 1 #最後の文字になると終わりなので１だけ引く\n        batch_size = inchis.size(0)\n        preds = torch.zeros(batch_size, seq_length, self.vocab_size).to(DEVICE) #予測した文字を格納するために作成\n        \n        for s in range(seq_length):\n            lstm_input = torch.cat((embeds[:, s], features), dim = 1) #s文字目と画像特徴量をくっつける\n            h, c = self.lstm(lstm_input, (h, c)) #hが予測されたデータ\n            x = self.drop(h)\n            x = self.linear(x)\n            preds[:, s] = x #s文字目の次の文字の予測\n        return preds\n\n    def predict(self, features, max_len): #画像特徴量、最大文字数\n        batch_size = features.size(0)\n        features = features.mean(dim = 1)\n        h = self.init_h(features)\n        c = self.init_c(features)\n        \n        word = torch.full((batch_size, 1), stoi[\"<sos>\"]).to(DEVICE) #1文字目は必ず\"sos\"\n        embeds = self.embedding(word)\n        preds = torch.zeros((batch_size, max_len), dtype = torch.long).to(DEVICE) #出力する予測の枠\n        preds[:, 0] = word.squeeze() #１文字目の\"sos\"を入れておくだけ\n        for i in range(max_len): #最大文字数まで予測を繰り返す\n            lstm_input = torch.cat((embeds[:, 0], features), dim = 1) #１つ前の文字と画像特徴量をくっつける\n            h, c = self.lstm(lstm_input, (h, c))\n            x = self.drop(h)\n            x = self.linear(x)\n            x = x.view(batch_size, -1)\n            pred_idx = x.argmax(dim = 1) #最も値の大きい語彙を予測とする\n            preds[:, i] = pred_idx #出力データに格納\n            embeds = self.embedding(pred_idx).unsqueeze(1) #次の文字の予測に使用する\n        return preds","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"predict関数を作りました。\n\n１つ前の文字をEmbeddingで変換して画像特徴量とh,cから次の文字hを出力する処理を最大文字数まで繰り返しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoder = Encoder().to(DEVICE)\ndecoder = Decoder(vocab_size = VOCAB_SIZE, embed_size = 256, encoder_dim = 512, decoder_dim = 512).to(DEVICE)\nencoder.load_state_dict(torch.load(\"./bms_encoder.pth\", map_location = DEVICE))\ndecoder.load_state_dict(torch.load(\"./bms_decoder.pth\", map_location = DEVICE))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最もlossの小さいモデルを保存しているので取り出します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"encoder.eval()\ndecoder.eval()\npreds = []\nfor images, inchis in tqdm(valid_loader):\n    images = images.to(DEVICE)\n    with torch.no_grad():\n        encoded = encoder(images)\n        pred = decoder.predict(encoded, max_len = MAX_LEN)\n        preds.append(pred)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"評価用データで予測してみます。\n\nここでMAX_LENが使われます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(preds[0].shape)\npreds[0][0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"予測したラベルを見てみましょう。よくわかりませんね。"},{"metadata":{"trusted":true},"cell_type":"code","source":"def generate_inchi(pred):\n    label = [itos[i] for i in pred.to(\"cpu\").numpy()]\n    result = []\n    for i in range(len(label)):\n        if label[i] == \"<eos>\":\n            break\n        result.append(label[i])\n    result = \"\".join(result)\n    return result\n\nresult = generate_inchi(preds[0][0])\nprint(result)\nprint(df_valid[\"InChI\"].values[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\"eos\"になるまで文字を取り出す関数を作ります。\n\nこれに予測ラベルを入れるとInChIになります。\n\n実際の文字と比べてみるとまぁまぁかなといったところ。"},{"metadata":{},"cell_type":"markdown","source":"# 9. SUBMIT"},{"metadata":{"trusted":true},"cell_type":"code","source":"test = pd.read_csv(\"../input/bms-molecular-translation/sample_submission.csv\")\ntest[\"file_path\"] = test[\"image_id\"].apply(\n    lambda x: f\"../input/bms-molecular-translation/test/{x[0]}/{x[1]}/{x[2]}/{x}.png\"\n)\nprint(test.shape)\ntest.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"提出するtestデータのパスを作成します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n        self.paths = self.df[\"file_path\"].values\n        \n    def __len__(self):\n        return self.df.shape[0]\n    \n    def __getitem__(self, idx):\n        path = self.paths[idx]\n        image = cv2.imread(path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image = A.Compose([\n            A.Resize(256, 256),\n            A.Normalize(),\n            ToTensorV2()\n        ])(image = image)[\"image\"]\n        return image\n    \n#test_data = TestDataset(test.iloc[:200, :])\ntest_data = TestDataset(test)\ntest_loader = DataLoader(test_data, batch_size = BATCH_SIZE * 4, shuffle = False, drop_last = False, num_workers = 4)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"testではInChIがない点だけ注意しましょう。collate_fnも不要です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"preds = []\nfor images in tqdm(test_loader):\n    with torch.no_grad():\n        images = images.to(DEVICE)\n        encoded = encoder(images)\n        pred = decoder.predict(encoded, max_len = MAX_LEN)\n        preds.append(pred)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"testから提出用の予測データを作ります。\n\nCPUだと絶望的に長いのでGPUを使いましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"generate_inchi(preds[0][0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"予測はこんな感じ。"},{"metadata":{"trusted":true},"cell_type":"code","source":"submit_preds = []\nfor pred in preds:\n    submit_preds.append([generate_inchi(p) for p in pred])\nsubmit_preds = np.concatenate(submit_preds, axis = 0)\nprint(submit_preds.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"各予測をまとめます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"#submit = test[[\"image_id\", \"InChI\"]].iloc[:200, :].copy()\nsubmit = test[[\"image_id\", \"InChI\"]].copy()\nsubmit[\"InChI\"] = submit_preds\nsubmit.to_csv(\"submission.csv\", index = False)\nsubmit","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"testから\"image_id\"と\"InChI\"を頂戴してさっきの予測データを\"InChI\"に入れましょう。\n\nこれで提出ができます。"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}