{"id":13628024,"url":"https://github.com/ThomasDelteil/TextClassificationCNNs_MXNet","last_synced_at":"2025-04-17T00:33:12.115Z","repository":{"id":71902817,"uuid":"124455208","full_name":"ThomasDelteil/TextClassificationCNNs_MXNet","owner":"ThomasDelteil","description":"CNN, NLP and MXNet/Gluon demo","archived":false,"fork":false,"pushed_at":"2018-12-03T19:21:27.000Z","size":132138,"stargazers_count":39,"open_issues_count":0,"forks_count":9,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-10-03T23:01:52.524Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://thomasdelteil.github.io/TextClassificationCNNs_MXNet/","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ThomasDelteil.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-08T22:22:06.000Z","updated_at":"2022-09-05T05:56:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"d9504482-1001-464f-add8-6da660225855","html_url":"https://github.com/ThomasDelteil/TextClassificationCNNs_MXNet","commit_stats":null,"previous_names":["thomasdelteil/cnn_nlp_mxnet"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThomasDelteil%2FTextClassificationCNNs_MXNet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThomasDelteil%2FTextClassificationCNNs_MXNet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThomasDelteil%2FTextClassificationCNNs_MXNet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThomasDelteil%2FTextClassificationCNNs_MXNet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ThomasDelteil","download_url":"https://codeload.github.com/ThomasDelteil/TextClassificationCNNs_MXNet/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223735249,"owners_count":17194069,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T22:00:43.110Z","updated_at":"2024-11-08T18:31:17.701Z","avatar_url":"https://github.com/ThomasDelteil.png","language":"Jupyter Notebook","funding_links":[],"categories":["\u003ca name=\"NLP\"\u003e\u003c/a\u003e3. NLP"],"sub_categories":["2.14 Misc"],"readme":"**This is an automated Markdown generation from the notebook '[Crepe-Gluon.ipynb](https://github.com/ThomasDelteil/CNN_NLP_MXNet/blob/master/Crepe-Gluon.ipynb)'**\n\nCheck the live demo [here](https://thomasdelteil.github.io/TextClassificationCNNs_MXNet/)!\n\nSlides available [here](https://www.slideshare.net/ThomasDelteil1/convolutional-neural-networks-and-natural-language-processing-90539354)\n\nRecordings available here, [part1](https://www.youtube.com/watch?v=RgIa3_BjGyk), [part2](https://www.youtube.com/watch?v=mN15vKIyfoA), [part3](https://www.youtube.com/watch?v=K120xBnY6OA).\n\n# Character-level Convolutional Networks for text Classification\n\n## Crepe model implementation with MXNet/Gluon\n\n\nThis is an implementation of [the crepe model, Character-level Convolutional Networks for Text Classification](https://arxiv.org/abs/1509.01626). That this is the paper we reference throughout the tutorial\n\nWe are going to perform a **text classification** task, trying to classify Amazon reviews according to the product category they belong to.\n\nThis work is inspired from a previous collaborative work with [Ilia Karmanov and Miguel Fierro](https://github.com/ilkarman/NLP-Sentiment)\n\n## Install Guide\nYou need to install [Apache MXNet](http://mxnet.incubator.apache.org/) in order to run this tutorial. The following lines should work in most platform but checkout the [Apache install](http://mxnet.incubator.apache.org/install/index.html) guide for more info, especially if you plan to use GPU\n\n\n```python\n# GPU install\n!pip install mxnet-cu90 pandas -q\n# CPU install\n#!pip install mxnet pandas -q\n```\n\n## Data download\nThe dataset has been made available on this website: http://jmcauley.ucsd.edu/data/amazon/, citation of relevant papers:\n\n**Ups and downs: Modeling the visual evolution of fashion trends with one-class collaborative filtering**\nR. He, J. McAuley\n*WWW*, 2016\n\n**Image-based recommendations on styles and substitutes**\nJ. McAuley, C. Targett, J. Shi, A. van den Hengel\n*SIGIR*, 2015\n\n\n\n\nWe are downloading a subset of the reviews, the k-core reviews, where k=5. That means that for each category, the dataset has been trimmed to only contain 5 reviews per individual product, and 5 reviews per user.\n\n\n```python\nbase_url = 'http://snap.stanford.edu/data/amazon/productGraph/categoryFiles/'\nprefix = 'reviews_'\nsuffix = '_5.json.gz'\nfolder = 'data'\ncategories = [\n    'Home_and_Kitchen', \"\"\n    'Books', \n    'CDs_and_Vinyl', \n    'Movies_and_TV', \n    'Cell_Phones_and_Accessories',\n    'Sports_and_Outdoors', \n    'Clothing_Shoes_and_Jewelry'\n]\n!mkdir -p $folder\nfor category in categories:\n    print(category)\n    url = base_url+prefix+category+suffix\n    !wget -P $folder $url -nc -nv\n```\n\n    Home_and_Kitchen\n    Books\n    CDs_and_Vinyl\n    Movies_and_TV\n    Cell_Phones_and_Accessories\n    Sports_and_Outdoors\n    Clothing_Shoes_and_Jewelry\n\n\n## Data Pre-processing\nWe need to perform some pre-processing steps in order to have the data in a format we can use for training (**X**,**Y**)\nIn order to speed up training and balance the dataset we will only use a subset of reviews for each category.\n\n### Load the data in memory\n\n\n```python\nMAX_ITEMS_PER_CATEGORY = 250000\n```\n\nHelper functions to read from the .json.gzip files\n\n\n```python\nimport pandas as pd\nimport gzip\n\ndef parse(path):\n    g = gzip.open(path, 'rb')\n    for line in g:\n        yield eval(line)\n\ndef get_dataframe(path, num_lines):\n    i = 0\n    df = {}\n    for d in parse(path):\n        if i \u003e num_lines:\n            break\n        df[i] = d\n        i += 1\n\n    return pd.DataFrame.from_dict(df, orient='index')\n```\n\n    /home/ec2-user/anaconda3/lib/python3.6/site-packages/matplotlib/__init__.py:962: UserWarning: Duplicate key in file \"/home/ec2-user/.config/matplotlib/matplotlibrc\", line #2\n      (fname, cnt))\n    /home/ec2-user/anaconda3/lib/python3.6/site-packages/matplotlib/__init__.py:962: UserWarning: Duplicate key in file \"/home/ec2-user/.config/matplotlib/matplotlibrc\", line #3\n      (fname, cnt))\n\n\nFor each category we load MAX_ITEMS_PER_CATEGORY by randomly sampling the files and shuffling\n\n\n```python\n# Loading data from file if exist\ntry:\n    data = pd.read_pickle('pickleddata.pkl')\nexcept:\n    data = None\n```\n\nIf the data is not available in the pickled file, we create it from scratch\n\n\n```python\nif data is None:\n    data = pd.DataFrame(data={'X':[],'Y':[]})\n    for index, category in enumerate(categories):\n        df = get_dataframe(\"{}/{}{}{}\".format(folder, prefix, category, suffix), MAX_ITEMS_PER_CATEGORY)    \n        # Each review's summary is prepended to the main review text\n        df = pd.DataFrame(data={'X':(df['summary']+' | '+df['reviewText'])[:MAX_ITEMS_PER_CATEGORY],'Y':index})\n        data = data.append(df)\n        print('{}:{} reviews'.format(category, len(df)))\n\n    # Shuffle the samples\n    data = data.sample(frac=1)\n    data.reset_index(drop=True, inplace=True)\n    # Saving the data in a pickled file\n    pd.to_pickle(data, 'pickleddata.pkl')\n```\n\nLet's visualize the data:\n\n\n```python\nprint('Value counts:\\n',data['Y'].value_counts())\nfor i,cat in enumerate(categories):\n    print(i, cat)\ndata.head()\n```\n\n    Value counts:\n     1.0    250000\n    6.0    250000\n    5.0    250000\n    3.0    250000\n    2.0    250000\n    0.0    250000\n    4.0    194439\n    Name: Y, dtype: int64\n    0 Home_and_Kitchen\n    1 Books\n    2 CDs_and_Vinyl\n    3 Movies_and_TV\n    4 Cell_Phones_and_Accessories\n    5 Sports_and_Outdoors\n    6 Clothing_Shoes_and_Jewelry\n\n\n\n\n\n\u003cdiv\u003e\n\u003ctable border=\"1\" class=\"dataframe\"\u003e\n  \u003cthead\u003e\n    \u003ctr style=\"text-align: right;\"\u003e\n      \u003cth\u003e\u003c/th\u003e\n      \u003cth\u003eX\u003c/th\u003e\n      \u003cth\u003eY\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003cth\u003e0\u003c/th\u003e\n      \u003ctd\u003eWhy didnt I find this sooner!!! | This product...\u003c/td\u003e\n      \u003ctd\u003e0.0\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003e1\u003c/th\u003e\n      \u003ctd\u003eThe only thing weighing it down is the second ...\u003c/td\u003e\n      \u003ctd\u003e2.0\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003e2\u003c/th\u003e\n      \u003ctd\u003eGood | Works very good with a patch pulled or ...\u003c/td\u003e\n      \u003ctd\u003e5.0\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003e3\u003c/th\u003e\n      \u003ctd\u003eGood mirror glasses | These are very reflectiv...\u003c/td\u003e\n      \u003ctd\u003e6.0\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003e4\u003c/th\u003e\n      \u003ctd\u003ecute, cushy, too small :( | Well, here's anoth...\u003c/td\u003e\n      \u003ctd\u003e6.0\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/div\u003e\n\n\n\n### Creating the dataset\n\n\n```python\nimport mxnet as mx\nfrom mxnet import nd, autograd, gluon\nfrom mxnet.gluon.data import ArrayDataset\nfrom mxnet.gluon.data import DataLoader\nimport numpy as np\nimport multiprocessing\n```\n\n    /home/ec2-user/anaconda3/lib/python3.6/site-packages/urllib3/contrib/pyopenssl.py:46: DeprecationWarning: OpenSSL.rand is deprecated - you should use os.urandom instead\n      import OpenSSL.SSL\n\n\nSetting up the parameters for the network\n\n\n```python\nALPHABET = list(\"abcdefghijklmnopqrstuvwxyz0123456789-,;.!?:'\\\"/\\\\|_@#$%^\u0026*~`+ =\u003c\u003e()[]{}\") # The 69 characters as specified in the paper\nALPHABET_INDEX = {letter: index for index, letter in enumerate(ALPHABET)} # { a: 0, b: 1, etc}\nFEATURE_LEN = 1014 # max-length in characters for one document\nNUM_WORKERS = multiprocessing.cpu_count() # number of workers used in the data loading\nBATCH_SIZE = 128 # number of documents per batch\n```\n\nAccording to the paper, each document needs to be encoded in the following manner:\n    - Truncate to 1014 characters\n    - Reverse the string\n    - One-hot encode based on the alphabet\n    \nThe following `encode` function does this for us\n\n\n```python\ndef encode(text):\n    encoded = np.zeros([len(ALPHABET), FEATURE_LEN], dtype='float32')\n    review = text.lower()[:FEATURE_LEN-1:-1]\n    i = 0\n    for letter in text:\n        if i \u003e= FEATURE_LEN:\n            break;\n        if letter in ALPHABET_INDEX:\n            encoded[ALPHABET_INDEX[letter]][i] = 1\n        i += 1\n    return encoded\n```\n\nThe MXNet DataSet and DataLoader API lets you create different worker to pre-fetch the data and encode it the way you want, in order to prevent your GPU from starving\n\n\n```python\nclass AmazonDataSet(ArrayDataset):\n    # We pre-process the documents on the fly\n    def __getitem__(self, idx):\n        return encode(self._data[0][idx]), self._data[1][idx]\n```\n\nWe split our data into a training and a testing dataset\n\n\n```python\nsplit = 0.8\nsplit_index = int(split*len(data))\ntrain_data_X = data['X'][:split_index].as_matrix()\ntrain_data_Y = data['Y'][:split_index].as_matrix()\ntest_data_X = data['X'][split_index:].as_matrix()\ntest_data_Y = data['Y'][split_index:].as_matrix()\ntrain_dataset = AmazonDataSet(train_data_X, train_data_Y)\ntest_dataset = AmazonDataSet(test_data_X, test_data_Y)\n```\n\nCreating the training and testing dataloader, with NUM_WORKERS set to the number of CPU core\n\n\n```python\ntrain_dataloader = DataLoader(train_dataset, shuffle=True, batch_size=BATCH_SIZE, num_workers=NUM_WORKERS, last_batch='discard')\n```\n\n\n```python\ntest_dataloader = DataLoader(test_dataset, shuffle=True, batch_size=BATCH_SIZE, num_workers=NUM_WORKERS, last_batch='discard')\n```\n\n## Creation of the network\n\nThe context will define where the training takes place, on the CPU or on the GPU\n\n\n```python\n# ctx = mx.cpu()\nctx = mx.gpu() # to run on GPU\n```\n\nWe create the network following the instructions describe in the paper, using the small feature and small output units configuration\n\n![img](data/diagram.png)\n![img](data/convolutional_layers.png)\n![img](data/dense_layer.png)\n\n\nBased on the paper we set the following parameters:\n\n\n```python\nNUM_FILTERS = 256 # number of convolutional filters per convolutional layer\nNUM_OUTPUTS = len(categories) # number of classes\nFULLY_CONNECTED = 1024 # number of unit in the fully connected dense layer\nDROPOUT_RATE = 0.5 # probability of node drop out\nLEARNING_RATE = 0.01 # learning rate of the gradient\nMOMENTUM = 0.9 # momentum of the gradient\nWDECAY = 0.00001 # regularization term to limit size of weights\n```\n\n\n```python\nnet = gluon.nn.HybridSequential()\nwith net.name_scope():\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=7, activation='relu'))\n    net.add(gluon.nn.MaxPool1D(pool_size=3, strides=3))\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=7, activation='relu'))\n    net.add(gluon.nn.MaxPool1D(pool_size=3, strides=3))\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=3, activation='relu'))\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=3, activation='relu'))\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=3, activation='relu'))\n    net.add(gluon.nn.Conv1D(channels=NUM_FILTERS, kernel_size=3, activation='relu'))\n    net.add(gluon.nn.MaxPool1D(pool_size=3, strides=3))\n    net.add(gluon.nn.Flatten())\n    net.add(gluon.nn.Dense(FULLY_CONNECTED, activation='relu'))\n    net.add(gluon.nn.Dropout(DROPOUT_RATE))\n    net.add(gluon.nn.Dense(FULLY_CONNECTED, activation='relu'))\n    net.add(gluon.nn.Dropout(DROPOUT_RATE))\n    net.add(gluon.nn.Dense(NUM_OUTPUTS))\n\n```\n\n\n```python\nprint(net)\n```\n\n    HybridSequential(\n      (0): Conv1D(None -\u003e 256, kernel_size=(7,), stride=(1,))\n      (1): MaxPool1D(size=(3,), stride=(3,), padding=(0,), ceil_mode=False)\n      (2): Conv1D(None -\u003e 256, kernel_size=(7,), stride=(1,))\n      (3): MaxPool1D(size=(3,), stride=(3,), padding=(0,), ceil_mode=False)\n      (4): Conv1D(None -\u003e 256, kernel_size=(3,), stride=(1,))\n      (5): Conv1D(None -\u003e 256, kernel_size=(3,), stride=(1,))\n      (6): Conv1D(None -\u003e 256, kernel_size=(3,), stride=(1,))\n      (7): Conv1D(None -\u003e 256, kernel_size=(3,), stride=(1,))\n      (8): MaxPool1D(size=(3,), stride=(3,), padding=(0,), ceil_mode=False)\n      (9): Flatten\n      (10): Dense(None -\u003e 1024, Activation(relu))\n      (11): Dropout(p = 0.5)\n      (12): Dense(None -\u003e 1024, Activation(relu))\n      (13): Dropout(p = 0.5)\n      (14): Dense(None -\u003e 7, linear)\n    )\n\n\nHere we define whether we load a pre-trained version of the model and [hybridize the network](https://mxnet.incubator.apache.org/tutorials/gluon/hybrid.html) for speed improvements\n\n\n```python\nhybridize = True # for speed improvement, compile the network but no in-depth debugging possible\nload_params = True # Load pre-trained model\n```\n\n### Parameter initialization\n\n\n```python\nif load_params:\n    net.load_params('crepe_gluon_epoch6.params', ctx=ctx)\nelse:\n    net.collect_params().initialize(mx.init.Xavier(magnitude=2.24), ctx=ctx)\n```\n\n### Hybridization\n\n\n```python\nif hybridize:\n    net.hybridize()\n```\n\n### Softmax cross-entropy Loss\n\nWe are in a multi-class classification problem, so we use the [Softmax Cross entropy loss](https://deepnotes.io/softmax-crossentropy)\n\n\n```python\nsoftmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()\n```\n\n### Optimizer\n\n\n```python\ntrainer = gluon.Trainer(net.collect_params(), 'sgd', \n                        {'learning_rate': LEARNING_RATE, \n                         'wd':WDECAY, \n                         'momentum':MOMENTUM})\n```\n\n### Evaluate Accuracy\n\n\n```python\ndef evaluate_accuracy(data_iterator, net):\n    acc = mx.metric.Accuracy()\n    for i, (data, label) in enumerate(data_iterator):\n        data = data.as_in_context(ctx)\n        label = label.as_in_context(ctx)\n        output = net(data)\n        prediction = nd.argmax(output, axis=1)\n\n        if (i%50 == 0):\n            print(\"Samples {}\".format(i*len(data)))\n        acc.update(preds=prediction, labels=label)\n    return acc.get()[1]\n```\n\n### Training Loop\nWe loop through the batches given by the data_loader. These batches have been asynchronously fetched by the workers.\n\nAfter an epoch, we measure the test_accuracy and save the parameters of the model\n\n\n```python\nstart_epoch = 6\nnumber_epochs = 7\nsmoothing_constant = .01\nfor e in range(start_epoch, number_epochs):\n    for i, (review, label) in enumerate(train_dataloader):\n        review = review.as_in_context(ctx)\n        label = label.as_in_context(ctx)\n        with autograd.record():\n            output = net(review)\n            loss = softmax_cross_entropy(output, label)\n        loss.backward()\n        trainer.step(review.shape[0])\n        \n        # moving average of the loss\n        curr_loss = nd.mean(loss).asscalar()\n        moving_loss = (curr_loss if (i == 0) \n                       else (1 - smoothing_constant) * moving_loss + (smoothing_constant) * curr_loss)\n\n        if (i%50 == 0):\n            nd.waitall()\n            print('Batch {}:{},{}'.format(i,curr_loss,moving_loss))\n\n    test_accuracy = evaluate_accuracy(test_dataloader, net)\n    #Save the model using the gluon params format\n    net.save_params('crepe_epoch_{}_test_acc_{}.params'.format(e,int(test_accuracy*10000)/100))\n    print(\"Epoch %s. Loss: %s, Test_acc %s\" % (e, moving_loss, test_accuracy))\n```\n\n### Export to the symbolic format\nThe `save_params()` method works for models trained in Gluon. \n\nHowever the `export()` function, exports it to a format usable in the symbolic API.\nWe need the symbolic API in order to make it compatible with the current version of MXNet Model Server, for deployment purposes\n\n\n```python\nnet.export('model/crepe')\n```\n\n### Random testing\n\nLet's randomly pick a few reviews and see how the classifier does!\n\n\n```python\nimport random\nindex = random.randint(1, len(data))\nreview = data['X'][index]\nlabel = categories[int(data['Y'][index])]\nprint(review)\nprint('\\nCategory: {}\\n'.format(label))\nencoded = nd.array([encode(review)], ctx=ctx)\noutput = net(encoded)\npredicted = categories[np.argmax(output[0].asnumpy())]\nif predicted == label:\n      print('Correct')\nelse:\n      print('Incorrectly predicted {}'.format(predicted))\n```\n\n    Fine Breadmaker | We have used this mainly for the standard and whole wheat modes.  Their recipes work fine; also fine with Pamela's bread mix.\n    \n    Category: Home_and_Kitchen\n    \n    Correct\n\n\n### Manual Testing\nWe can also write our own reviews, encode them and see what the model predicts\n\n\n```python\nreview_title = \"Good stuff\"\nreview = \"This album is definitely better than the previous one\"\n```\n\n\n```python\nprint(review_title)\nprint(review + '\\n')\nencoded = nd.array([encode(review + \" | \" + review_title)], ctx=ctx)\noutput = net(encoded)\nsoftmax = nd.exp(output) / nd.sum(nd.exp(output))[0]\npredicted = categories[np.argmax(output[0].asnumpy())]\nprint('Predicted: {}\\n'.format(predicted))\nfor i, val in enumerate(categories):\n    print(val, float(int(softmax[0][i].asnumpy()*1000)/10), '%')\n```\n\n    Good stuff\n    This album is definitely better than the previous one\n    \n    Predicted: CDs_and_Vinyl\n    \n    Home_and_Kitchen 0.0 %\n    Books 0.0 %\n    CDs_and_Vinyl 98.7 %\n    Movies_and_TV 0.8 %\n    Cell_Phones_and_Accessories 0.2 %\n    Sports_and_Outdoors 0.1 %\n    Clothing_Shoes_and_Jewelry 0.0 %\n\n\n# Model Deployment\n\nHead over to the `model/` folder and have a look at the README.md to learn how you can deploy this pre-trained model to MXNet Model Server. You can then package the API in a docker container for cloud deployment!\n\nAn interactive live demo is available [here](https://thomasdelteil.github.io/TextClassificationCNNs_MXNet/)\n\n[![img](https://user-images.githubusercontent.com/3716307/48382335-6099ae00-e695-11e8-8110-f692b9ecb831.png)](https://thomasdelteil.github.io/TextClassificationCNNs_MXNet/)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FThomasDelteil%2FTextClassificationCNNs_MXNet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FThomasDelteil%2FTextClassificationCNNs_MXNet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FThomasDelteil%2FTextClassificationCNNs_MXNet/lists"}