{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Text Sentiment Classification: Using Convolutional Neural Networks (textCNN)\n",
"\n",
"In the \"Convolutional Neural Networks\" chapter, we explored how to process two-dimensional image data with two-dimensional convolutional neural networks. In the previous language models and text classification tasks, we treated text data as a time series with only one dimension, and naturally, we used recurrent neural networks to process such data. In fact, we can also treat text as a one-dimensional image, so that we can use one-dimensional convolutional neural networks to capture associations between adjacent words. This section describes a groundbreaking approach to applying convolutional neural networks to text analysis: textCNN[1]. First, import the packages and modules required for the experiment."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "2"
}
},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import d2l\n",
"from mxnet import gluon, init, nd\n",
"from mxnet.contrib import text\n",
"from mxnet.gluon import loss as gloss, nn"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## One-dimensional Convolutional Layer\n",
"\n",
"Before introducing the model, let us explain how a one-dimensional convolutional layer works. Like a two-dimensional convolutional layer, a one-dimensional convolutional layer uses a one-dimensional cross-correlation operation. In the one-dimensional cross-correlation operation, the convolution window starts from the leftmost side of the input array and slides on the input array from left to right successively. When the convolution window slides to a certain position, the input subarray in the window and kernel array are multiplied and summed by element to get the element at the corresponding location in the output array. As shown in Figure 12.4, the input is a one-dimensional array with a width of 7 and the width of the kernel array is 2. As we can see, the output width is $7-2+1=6$ and the first element is obtained by performing multiplication by element on the leftmost input subarray with a width of 2 and kernel array and then summing the results.\n",
"\n",
"![One-dimensional cross-correlation operation. The shaded parts are the first output element as well as the input and kernel array elements used in its calculation: $0\\times1+1\\times2=2$. ](../img/conv1d.svg)\n",
"\n",
"Next, we implement one-dimensional cross-correlation in the `corr1d` function. It accepts the input array `X` and kernel array `K` and outputs the array `Y`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "3"
}
},
"outputs": [],
"source": [
"def corr1d(X, K):\n",
" w = K.shape[0]\n",
" Y = nd.zeros((X.shape[0] - w + 1))\n",
" for i in range(Y.shape[0]):\n",
" Y[i] = (X[i: i + w] * K).sum()\n",
" return Y"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we will reproduce the results of the one-dimensional cross-correlation operation in Figure 12.4."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "4"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\n",
"[ 2. 5. 8. 11. 14. 17.]\n",
""
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"X, K = nd.array([0, 1, 2, 3, 4, 5, 6]), nd.array([1, 2])\n",
"corr1d(X, K)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The one-dimensional cross-correlation operation for multiple input channels is also similar to the two-dimensional cross-correlation operation for multiple input channels. On each channel, it performs the one-dimensional cross-correlation operation on the kernel and its corresponding input and adds the results of the channels to get the output. Figure 12.5 shows a one-dimensional cross-correlation operation with three input channels.\n",
"\n",
"![One-dimensional cross-correlation operation with three input channels. The shaded parts are the first output element as well as the input and kernel array elements used in its calculation: $0\\times1+1\\times2+1\\times3+2\\times4+2\\times(-1)+3\\times(-3)=2$. ](../img/conv1d-channel.svg)\n",
"\n",
"Now, we reproduce the results of the one-dimensional cross-correlation operation with multi-input channel in Figure 12.5."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "5"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\n",
"[ 2. 8. 14. 20. 26. 32.]\n",
""
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def corr1d_multi_in(X, K):\n",
" # First, we traverse along the 0th dimension (channel dimension) of X and\n",
" # K. Then, we add them together by using * to turn the result list into a\n",
" # positional argument of the add_n function\n",
" return nd.add_n(*[corr1d(x, k) for x, k in zip(X, K)])\n",
"\n",
"X = nd.array([[0, 1, 2, 3, 4, 5, 6],\n",
" [1, 2, 3, 4, 5, 6, 7],\n",
" [2, 3, 4, 5, 6, 7, 8]])\n",
"K = nd.array([[1, 2], [3, 4], [-1, -3]])\n",
"corr1d_multi_in(X, K)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The definition of a two-dimensional cross-correlation operation tells us that a one-dimensional cross-correlation operation with multiple input channels can be regarded as a two-dimensional cross-correlation operation with a single input channel. As shown in Figure 12.6, we can also present the one-dimensional cross-correlation operation with multiple input channels in Figure 12.5 as the equivalent two-dimensional cross-correlation operation with a single input channel. Here, the height of the kernel is equal to the height of the input.\n",
"\n",
"![Two-dimensional cross-correlation operation with a single input channel. The highlighted parts are the first output element and the input and kernel array elements used in its calculation: $2\\times(-1)+3\\times(-3)+1\\times3+2\\times4+0\\times1+1\\times2=2$. ](../img/conv1d-2d.svg)\n",
"\n",
"Both the outputs in Figure 12.4 and Figure 12.5 have only one channel. We discussed how to specify multiple output channels in a two-dimensional convolutional layer in the [“Multiple Input and Output Channels”](../chapter_convolutional-neural-networks/channels.md)section. Similarly, we can also specify multiple output channels in the one-dimensional convolutional layer to extend the model parameters in the convolutional layer.\n",
"\n",
"\n",
"## Max-Over-Time Pooling Layer\n",
"\n",
"Similarly, we have a one-dimensional pooling layer. The max-over-time pooling layer used in TextCNN actually corresponds to a one-dimensional global maximum pooling layer. Assuming that the input contains multiple channels, and each channel consists of values on different time steps, the output of each channel will be the largest value of all time steps in the channel. Therefore, the input of the max-over-time pooling layer can have different time steps on each channel.\n",
"\n",
"To improve computing performance, we often combine timing examples of different lengths into a mini-batch and make the lengths of each timing example in the batch consistent by appending special characters (such as 0) to the end of shorter examples. Naturally, the added special characters have no intrinsic meaning. Because the main purpose of the max-over-time pooling layer is to capture the most important features of timing, it usually allows the model to be unaffected by the manually added characters.\n",
"\n",
"\n",
"## Read and Preprocess IMDb Data Sets\n",
"\n",
"We still use the same IMDb data set as n the previous section for sentiment analysis. The following steps for reading and preprocessing the data set are the same as in the previous section."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "2"
}
},
"outputs": [],
"source": [
"vocab, train_iter, test_iter = d2l.load_data_imdb(batch_size=64)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The TextCNN Model\n",
"\n",
"TextCNN mainly uses a one-dimensional convolutional layer and max-over-time pooling layer. Suppose the input text sequence consists of $n$ words, and each word is represented by a $d$-dimension word vector. Then the input example has a width of $n$, a height of 1, and $d$ input channels. The calculation of textCNN can be mainly divided into the following steps:\n",
"\n",
"1. Define multiple one-dimensional convolution kernels and use them to perform convolution calculations on the inputs. Convolution kernels with different widths may capture the correlation of different numbers of adjacent words.\n",
"2. Perform max-over-time pooling on all output channels, and then concatenate the pooling output values of these channels in a vector.\n",
"3. The concatenated vector is transformed into the output for each category through the fully connected layer. A dropout layer can be used in this step to deal with overfitting.\n",
"\n",
"![TextCNN design. ](../img/textcnn.svg)\n",
"\n",
"Figure 12.7 gives an example to illustrate the textCNN. The input here is a sentence with 11 words, with each word represented by a 6-dimensional word vector. Therefore, the input sequence has a width of 11 and 6 input channels. We assume there are two one-dimensional convolution kernels with widths of 2 and 4, and 4 and 5 output channels, respectively. Therefore, after one-dimensional convolution calculation, the width of the four output channels is $11-2+1=10$, while the width of the other five channels is $11-4+1=8$. Even though the width of each channel is different, we can still perform max-over-time pooling for each channel and concatenate the pooling outputs of the 9 channels into a 9-dimensional vector. Finally, we use a fully connected layer to transform the 9-dimensional vector into a 2-dimensional output: positive sentiment and negative sentiment predictions.\n",
"\n",
"Next, we will implement a textCNN model. Compared with the previous section, in addition to replacing the recurrent neural network with a one-dimensional convolutional layer, here we use two embedding layers, one with a fixed weight and another that participates in training."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "10"
}
},
"outputs": [],
"source": [
"class TextCNN(nn.Block):\n",
" def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels,\n",
" **kwargs):\n",
" super(TextCNN, self).__init__(**kwargs)\n",
" self.embedding = nn.Embedding(vocab_size, embed_size)\n",
" # The embedding layer does not participate in training\n",
" self.constant_embedding = nn.Embedding(vocab_size, embed_size)\n",
" self.dropout = nn.Dropout(0.5)\n",
" self.decoder = nn.Dense(2)\n",
" # The max-over-time pooling layer has no weight, so it can share an\n",
" # instance\n",
" self.pool = nn.GlobalMaxPool1D()\n",
" # Create multiple one-dimensional convolutional layers\n",
" self.convs = nn.Sequential()\n",
" for c, k in zip(num_channels, kernel_sizes):\n",
" self.convs.add(nn.Conv1D(c, k, activation='relu'))\n",
"\n",
" def forward(self, inputs):\n",
" # Concatenate the output of two embedding layers with shape of\n",
" # (batch size, number of words, word vector dimension) by word vector\n",
" embeddings = nd.concat(\n",
" self.embedding(inputs), self.constant_embedding(inputs), dim=2)\n",
" # According to the input format required by Conv1D, the word vector\n",
" # dimension, that is, the channel dimension of the one-dimensional\n",
" # convolutional layer, is transformed into the previous dimension\n",
" embeddings = embeddings.transpose((0, 2, 1))\n",
" # For each one-dimensional convolutional layer, after max-over-time\n",
" # pooling, an NDArray with the shape of (batch size, channel size, 1)\n",
" # can be obtained. Use the flatten function to remove the last\n",
" # dimension and then concatenate on the channel dimension\n",
" encoding = nd.concat(*[nd.flatten(\n",
" self.pool(conv(embeddings))) for conv in self.convs], dim=1)\n",
" # After applying the dropout method, use a fully connected layer to\n",
" # obtain the output\n",
" outputs = self.decoder(self.dropout(encoding))\n",
" return outputs"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a TextCNN instance. It has 3 convolutional layers with kernel widths of 3, 4, and 5, all with 100 output channels."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]\n",
"ctx = d2l.try_all_gpus()\n",
"net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)\n",
"net.initialize(init.Xavier(), ctx=ctx)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Load Pre-trained Word Vectors\n",
"\n",
"As in the previous section, load pre-trained 100-dimensional GloVe word vectors and initialize the embedding layers `embedding` and `constant_embedding`. Here, the former participates in training while the latter has a fixed weight."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"glove_embedding = text.embedding.create(\n",
" 'glove', pretrained_file_name='glove.6B.100d.txt')\n",
"embeds = glove_embedding.get_vecs_by_tokens(vocab.idx_to_token)\n",
"net.embedding.weight.set_data(embeds)\n",
"net.embedding.collect_params().setattr('grad_req', 'null')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Train and Evaluate the Model\n",
"\n",
"Now we can train the model."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "30"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"training on [gpu(0), gpu(1), gpu(2), gpu(3)]\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 1, loss 0.5046, train acc 0.749, test acc 0.872, time 12.4 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 2, loss 0.2252, train acc 0.911, test acc 0.870, time 12.1 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 3, loss 0.0871, train acc 0.970, test acc 0.859, time 12.0 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 4, loss 0.0339, train acc 0.990, test acc 0.853, time 12.1 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 5, loss 0.0125, train acc 0.997, test acc 0.843, time 11.8 sec\n"
]
}
],
"source": [
"lr, num_epochs = 0.001, 5\n",
"trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})\n",
"loss = gloss.SoftmaxCrossEntropyLoss()\n",
"d2l.train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below, we use the trained model to the classify sentiments of two simple sentences."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'positive'"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d2l.predict_sentiment(net, vocab, 'this movie is so great')"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'negative'"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d2l.predict_sentiment(net, vocab, 'this movie is so bad')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"* We can use one-dimensional convolution to process and analyze timing data.\n",
"* A one-dimensional cross-correlation operation with multiple input channels can be regarded as a two-dimensional cross-correlation operation with a single input channel.\n",
"* The input of the max-over-time pooling layer can have different numbers of time steps on each channel.\n",
"* TextCNN mainly uses a one-dimensional convolutional layer and max-over-time pooling layer.\n",
"\n",
"\n",
"## Exercises\n",
"\n",
"* Tune the hyper-parameters and compare the two sentiment analysis methods, using recurrent neural networks and using convolutional neural networks, as regards accuracy and operational efficiency.\n",
"* Can you further improve the accuracy of the model on the test set by using the three methods introduced in the previous section: tuning hyper-parameters, using larger pre-trained word vectors, and using the spaCy word tokenization tool?\n",
"* What other natural language processing tasks can you use textCNN for?\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"## Reference\n",
"\n",
"[1] Kim, Y. (2014). Convolutional neural networks for sentence classification. arXiv preprint arXiv:1408.5882.\n",
"\n",
"## Scan the QR Code to [Discuss](https://discuss.mxnet.io/t/2392)\n",
"\n",
"![](../img/qr_sentiment-analysis-cnn.svg)"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}