{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 10.7 文本情感分类：使用循环神经网络"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:23.247619Z",
     "start_time": "2019-07-03T04:26:20.949830Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.0.0 cuda\n"
     ]
    }
   ],
   "source": [
    "import collections\n",
    "import os\n",
    "import random\n",
    "import tarfile\n",
    "import torch\n",
    "from torch import nn\n",
    "import torchtext.vocab as Vocab\n",
    "import torch.utils.data as Data\n",
    "\n",
    "import sys\n",
    "sys.path.append(\"..\") \n",
    "import d2lzh_pytorch as d2l\n",
    "\n",
    "os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"7\"\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "DATA_ROOT = \"/S1/CSCL/tangss/Datasets\"\n",
    "\n",
    "print(torch.__version__, device)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10.7.1 文本情感分类数据\n",
    "### 10.7.1.1 读取数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:23.255913Z",
     "start_time": "2019-07-03T04:26:23.250957Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "fname = os.path.join(DATA_ROOT, \"aclImdb_v1.tar.gz\")\n",
    "if not os.path.exists(os.path.join(DATA_ROOT, \"aclImdb\")):\n",
    "    print(\"从压缩包解压...\")\n",
    "    with tarfile.open(fname, 'r') as f:\n",
    "        f.extractall(DATA_ROOT)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:39.257587Z",
     "start_time": "2019-07-03T04:26:23.258808Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 12500/12500 [00:04<00:00, 2930.03it/s]\n",
      "100%|██████████| 12500/12500 [00:04<00:00, 3008.48it/s]\n",
      "100%|██████████| 12500/12500 [00:03<00:00, 3365.08it/s]\n",
      "100%|██████████| 12500/12500 [00:03<00:00, 3305.63it/s]\n"
     ]
    }
   ],
   "source": [
    "from tqdm import tqdm\n",
    "def read_imdb(folder='train', data_root=\"/S1/CSCL/tangss/Datasets/aclImdb\"):  # 本函数已保存在d2lzh_pytorch包中方便以后使用\n",
    "    data = []\n",
    "    for label in ['pos', 'neg']:\n",
    "        folder_name = os.path.join(data_root, folder, label)\n",
    "        for file in tqdm(os.listdir(folder_name)):\n",
    "            with open(os.path.join(folder_name, file), 'rb') as f:\n",
    "                review = f.read().decode('utf-8').replace('\\n', '').lower()\n",
    "                data.append([review, 1 if label == 'pos' else 0])\n",
    "    random.shuffle(data)\n",
    "    return data\n",
    "\n",
    "train_data, test_data = read_imdb('train'), read_imdb('test')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10.7.1.2 预处理数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:39.262666Z",
     "start_time": "2019-07-03T04:26:39.259588Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def get_tokenized_imdb(data):  # 本函数已保存在d2lzh_pytorch包中方便以后使用\n",
    "    \"\"\"\n",
    "    data: list of [string, label]\n",
    "    \"\"\"\n",
    "    def tokenizer(text):\n",
    "        return [tok.lower() for tok in text.split(' ')]\n",
    "    return [tokenizer(review) for review, _ in data]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:42.010298Z",
     "start_time": "2019-07-03T04:26:39.264464Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('# words in vocab:', 46151)"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def get_vocab_imdb(data):  # 本函数已保存在d2lzh_pytorch包中方便以后使用\n",
    "    tokenized_data = get_tokenized_imdb(data)\n",
    "    counter = collections.Counter([tk for st in tokenized_data for tk in st])\n",
    "    return Vocab.Vocab(counter, min_freq=5)\n",
    "\n",
    "vocab = get_vocab_imdb(train_data)\n",
    "'# words in vocab:', len(vocab)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:42.016214Z",
     "start_time": "2019-07-03T04:26:42.012406Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def preprocess_imdb(data, vocab):  # 本函数已保存在d2lzh_torch包中方便以后使用\n",
    "    max_l = 500  # 将每条评论通过截断或者补0，使得长度变成500\n",
    "\n",
    "    def pad(x):\n",
    "        return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))\n",
    "\n",
    "    tokenized_data = get_tokenized_imdb(data)\n",
    "    features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in tokenized_data])\n",
    "    labels = torch.tensor([score for _, score in data])\n",
    "    return features, labels"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10.7.1.3 创建数据迭代器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:47.614720Z",
     "start_time": "2019-07-03T04:26:42.017922Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "batch_size = 64\n",
    "train_set = Data.TensorDataset(*preprocess_imdb(train_data, vocab))\n",
    "test_set = Data.TensorDataset(*preprocess_imdb(test_data, vocab))\n",
    "train_iter = Data.DataLoader(train_set, batch_size, shuffle=True)\n",
    "test_iter = Data.DataLoader(test_set, batch_size)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:47.624512Z",
     "start_time": "2019-07-03T04:26:47.616891Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "X torch.Size([64, 500]) y torch.Size([64])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "('#batches:', 391)"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "for X, y in train_iter:\n",
    "    print('X', X.shape, 'y', y.shape)\n",
    "    break\n",
    "'#batches:', len(train_iter)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10.7.2 使用循环神经网络的模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:47.630109Z",
     "start_time": "2019-07-03T04:26:47.625789Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class BiRNN(nn.Module):\n",
    "    def __init__(self, vocab, embed_size, num_hiddens, num_layers):\n",
    "        super(BiRNN, self).__init__()\n",
    "        self.embedding = nn.Embedding(len(vocab), embed_size)\n",
    "        \n",
    "        # bidirectional设为True即得到双向循环神经网络\n",
    "        self.encoder = nn.LSTM(input_size=embed_size, \n",
    "                                hidden_size=num_hiddens, \n",
    "                                num_layers=num_layers,\n",
    "                                bidirectional=True)\n",
    "        self.decoder = nn.Linear(4*num_hiddens, 2) # 初始时间步和最终时间步的隐藏状态作为全连接层输入\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        # inputs的形状是(批量大小, 词数)，因为LSTM需要将序列长度(seq_len)作为第一维，所以将输入转置后\n",
    "        # 再提取词特征，输出形状为(词数, 批量大小, 词向量维度)\n",
    "        embeddings = self.embedding(inputs.permute(1, 0))\n",
    "        # rnn.LSTM只传入输入embeddings，因此只返回最后一层的隐藏层在各时间步的隐藏状态。\n",
    "        # outputs形状是(词数, 批量大小, 2 * 隐藏单元个数)\n",
    "        outputs, _ = self.encoder(embeddings) # output, (h, c)\n",
    "        # 连结初始时间步和最终时间步的隐藏状态作为全连接层输入。它的形状为\n",
    "        # (批量大小, 4 * 隐藏单元个数)。\n",
    "        encoding = torch.cat((outputs[0], outputs[-1]), -1)\n",
    "        outs = self.decoder(encoding)\n",
    "        return outs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:47.684133Z",
     "start_time": "2019-07-03T04:26:47.631441Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "embed_size, num_hiddens, num_layers = 100, 100, 2\n",
    "net = BiRNN(vocab, embed_size, num_hiddens, num_layers)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10.7.2.1 加载预训练的词向量"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:47.895604Z",
     "start_time": "2019-07-03T04:26:47.685801Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "glove_vocab = Vocab.GloVe(name='6B', dim=100, cache=os.path.join(DATA_ROOT, \"glove\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:26:48.102388Z",
     "start_time": "2019-07-03T04:26:47.897582Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def load_pretrained_embedding(words, pretrained_vocab):\n",
    "    \"\"\"从预训练好的vocab中提取出words对应的词向量\"\"\"\n",
    "    embed = torch.zeros(len(words), pretrained_vocab.vectors[0].shape[0]) # 初始化为0\n",
    "    oov_count = 0 # out of vocabulary\n",
    "    for i, word in enumerate(words):\n",
    "        try:\n",
    "            idx = pretrained_vocab.stoi[word]\n",
    "            embed[i, :] = pretrained_vocab.vectors[idx]\n",
    "        except KeyError:\n",
    "            oov_count += 0\n",
    "    if oov_count > 0:\n",
    "        print(\"There are %d oov words.\")\n",
    "    return embed\n",
    "\n",
    "net.embedding.weight.data.copy_(load_pretrained_embedding(vocab.itos, glove_vocab))\n",
    "net.embedding.weight.requires_grad = False # 直接加载预训练好的, 所以不需要更新它"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10.7.2.2 训练并评价模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:47:57.808046Z",
     "start_time": "2019-07-03T04:26:48.104185Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "training on  cuda\n",
      "epoch 1, loss 0.5759, train acc 0.666, test acc 0.832, time 250.8 sec\n",
      "epoch 2, loss 0.1785, train acc 0.842, test acc 0.852, time 253.3 sec\n",
      "epoch 3, loss 0.1042, train acc 0.866, test acc 0.856, time 253.7 sec\n",
      "epoch 4, loss 0.0682, train acc 0.888, test acc 0.868, time 254.2 sec\n",
      "epoch 5, loss 0.0483, train acc 0.901, test acc 0.862, time 251.4 sec\n"
     ]
    }
   ],
   "source": [
    "lr, num_epochs = 0.01, 5\n",
    "optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=lr)\n",
    "loss = nn.CrossEntropyLoss()\n",
    "d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:47:57.813888Z",
     "start_time": "2019-07-03T04:47:57.810244Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 本函数已保存在d2lzh包中方便以后使用\n",
    "def predict_sentiment(net, vocab, sentence):\n",
    "    \"\"\"sentence是词语的列表\"\"\"\n",
    "    device = list(net.parameters())[0].device\n",
    "    sentence = torch.tensor([vocab.stoi[word] for word in sentence], device=device)\n",
    "    label = torch.argmax(net(sentence.view((1, -1))), dim=1)\n",
    "    return 'positive' if label.item() == 1 else 'negative'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:47:57.829262Z",
     "start_time": "2019-07-03T04:47:57.815487Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'positive'"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-07-03T04:47:57.838439Z",
     "start_time": "2019-07-03T04:47:57.830707Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'negative'"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:py36]",
   "language": "python",
   "name": "conda-env-py36-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
