<jupyter_start><jupyter_text>An Introduction to SageMaker Neural Topic Model***Unsupervised representation learning and topic extraction using Neural Topic Model***1. [Introduction](Introduction)1. [Data Preparation](Data-Preparation)1. [Model Training](Model-Training)1. [Model Hosting and Inference](Model-Hosting-and-Inference)1. [Model Exploration](Model-Exploration)<jupyter_code># update sagemake package, in order to use write_spmatrix_to_sparse_tensor in the next cell
# !pip install -U sagemaker
# !pip install konlpy<jupyter_output><empty_output><jupyter_text>Fetching Data SetFirst let's define the folder to hold the data and clean the content in it which might be from previous experiments.<jupyter_code>%store -r s3_negative_path
import pandas as pd
df = pd.read_csv(s3_negative_path)
data = df['document'].tolist()
data[0:13]
len(data)<jupyter_output><empty_output><jupyter_text>---In the following cell, we use a tokenizer and a lemmatizer from `nltk`. In the list comprehension, we implement a simple rule: only consider words that are longer than 2 characters, start with a letter and match the `token_pattern`.<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer

import numpy as np
np.random.seed(0)
 
from konlpy.tag import Twitter
twitter = Twitter()
 
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.metrics.pairwise import linear_kernel, cosine_similarity
 
# tokenizer : 문장에서 색인어 추출을 위해 명사,동사,알파벳,숫자 정도의 단어만 뽑아서 normalization, stemming 처리하도록 함
def tokenizer(raw, pos=["Noun","Alpha","Verb","Number"], stopword=[]):
    return [
        word for word, tag in twitter.pos(
            raw, 
            norm=True,   # normalize 그랰ㅋㅋ -> 그래ㅋㅋ
            stem=True    # stemming 바뀌나->바뀌다
            )
            if len(word) > 1 and tag in pos and word not in stopword
        ]
%%time 

vocab_size = 2000
vectorize = CountVectorizer(
    tokenizer=tokenizer, 
    min_df = 2,
    max_df = 0.8,
                # 예제로 보기 좋게 1번 정도만 노출되는 단어들은 무시하기로 했다
                # min_df = 0.01 : 문서의 1% 미만으로 나타나는 단어 무시
                # min_df = 10 : 문서에 10개 미만으로 나타나는 단어 무시
                # max_df = 0.80 : 문서의 80% 이상에 나타나는 단어 무시
                # max_df = 10 : 10개 이상의 문서에 나타나는 단어 무시
    max_features = vocab_size
)
 
# 문장에서 노출되는 feature(특징이 될만한 단어) 수를 합한 Document Term Matrix(이하 DTM) 을 리턴한다
X = vectorize.fit_transform(data)
 
print(
    'fit_transform, (sentence {}, feature {})'.format(X.shape[0], X.shape[1])
)
# fit_transform, (sentence 5, feature 7)
  
print(X.toarray())


vocab_list = vectorize.get_feature_names()
print('vocab size:', len(vocab_list))
vocab_list[300:310]
def prepro_data(raw_vectors, threshold = 10):
    '''
    threshold 만큼의 단어이하의 문장은 삭제
    '''
    vectors = raw_vectors.copy()
    threshold = 2
    vectors = vectors[np.array(vectors.sum(axis=1)>threshold).reshape(-1,)]
    print('removed short docs (<{} words)'.format(threshold))        
    print(vectors.shape)
    return vectors

vectors = prepro_data(X)<jupyter_output>removed short docs (<2 words)
(6947, 2000)<jupyter_text>The output from `CountVectorizer` are sparse matrices with their elements being integers.<jupyter_code>print(type(vectors), vectors.dtype)
print(vectors[0])<jupyter_output><class 'scipy.sparse.csr.csr_matrix'> int64
  (0, 478)	1
  (0, 1677)	1
  (0, 695)	1<jupyter_text>Because all the parameters (weights and biases) in the NTM model are `np.float32` type we'd need the input data to also be in `np.float32`. It is better to do this type-casting upfront rather than repeatedly casting during mini-batch training.<jupyter_code>import scipy.sparse as sparse
vectors = sparse.csr_matrix(vectors, dtype=np.float32)
print(type(vectors), vectors.dtype)<jupyter_output><class 'scipy.sparse.csr.csr_matrix'> float32<jupyter_text>As a common practice in modeling training, we should have a training set, a validation set, and a test set. The training set is the set of data the model is actually being trained on. But what we really care about is not the model's performance on training set but its performance on future, unseen data. Therefore, during training, we periodically calculate scores (or losses) on the validation set to validate the performance of the model on unseen data. By assessing the model's ability to generalize we can stop the training at the optimal point via early stopping to avoid over-training. Note that when we only have a training set and no validation set, the NTM model will rely on scores on the training set to perform early stopping, which could result in over-training. Therefore, we recommend always supply a validation set to the model.Here we use 80% of the data set as the training set and the rest for validation set and test set. We will use the validation set in training and use the test set for demonstrating model inference.<jupyter_code>n_train = int(0.8 * vectors.shape[0])

# split train and test
train_vectors = vectors[:n_train, :]
test_vectors = vectors[n_train:, :]

# further split test set into validation set (val_vectors) and test  set (test_vectors)
n_test = test_vectors.shape[0]
val_vectors = test_vectors[:n_test//2, :]
test_vectors = test_vectors[n_test//2:, :]
print(train_vectors.shape, test_vectors.shape, val_vectors.shape)<jupyter_output>(5557, 2000) (695, 2000) (695, 2000)<jupyter_text>Setup AWS CredentialsWe first need to specify data locations and access roles. In particular, we need the following data:- The S3 `bucket` and `prefix` that you want to use for training and model data.  This should be within the same region as the Notebook Instance, training, and hosting.- The IAM `role` is used to give training and hosting access to your data. See the documentation for how to create these.  Note, if more than one role is required for notebook instances, training, and/or hosting, please replace the boto regexp with a the appropriate full IAM role arn string(s).<jupyter_code>import os
import sagemaker
from sagemaker import get_execution_role

role = get_execution_role()

sess = sagemaker.Session()
bucket=sess.default_bucket()
prefix = 'ml-architect/ntm/naver-movie-review'

train_prefix = os.path.join(prefix, 'train')
val_prefix = os.path.join(prefix, 'val')
output_prefix = os.path.join(prefix, 'output')

s3_train_data = os.path.join('s3://', bucket, train_prefix)
s3_val_data = os.path.join('s3://', bucket, val_prefix)
output_path = os.path.join('s3://', bucket, output_prefix)
print('Training set location', s3_train_data)
print('Validation set location', s3_val_data)
print('Trained model will be saved at', output_path)<jupyter_output>Training set location s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train
Validation set location s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/val
Trained model will be saved at s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/output<jupyter_text>Here we define a helper function to convert the data to RecordIO Protobuf format and upload it to S3. In addition, we will have the option to split the data into several parts specified by `n_parts`.The algorithm inherently supports multiple files in the training folder ("channel"), which could be very helpful for large data set. In addition, when we use distributed training with multiple workers (compute instances), having multiple files allows us to distribute different portions of the training data to different workers conveniently.Inside this helper function we use `write_spmatrix_to_sparse_tensor` function provided by [SageMaker Python SDK](https://github.com/aws/sagemaker-python-sdk) to convert scipy sparse matrix into RecordIO Protobuf format.<jupyter_code>def split_convert_upload(sparray, bucket, prefix, fname_template='data_part{}.pbr', n_parts=2):
    import io
    import boto3
    import sagemaker.amazon.common as smac
    
    chunk_size = sparray.shape[0]// n_parts
    for i in range(n_parts):

        # Calculate start and end indices
        start = i*chunk_size
        end = (i+1)*chunk_size
        if i+1 == n_parts:
            end = sparray.shape[0]
        
        # Convert to record protobuf
        buf = io.BytesIO()
        smac.write_spmatrix_to_sparse_tensor(array=sparray[start:end], file=buf, labels=None)
        buf.seek(0)
        
        # Upload to s3 location specified by bucket and prefix
        fname = os.path.join(prefix, fname_template.format(i))
        boto3.resource('s3').Bucket(bucket).Object(fname).upload_fileobj(buf)
        print('Uploaded data to s3://{}'.format(os.path.join(bucket, fname)))
split_convert_upload(train_vectors, bucket=bucket, prefix=train_prefix, fname_template='train_part{}.pbr', n_parts=8)
split_convert_upload(val_vectors, bucket=bucket, prefix=val_prefix, fname_template='val_part{}.pbr', n_parts=1)<jupyter_output>Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part0.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part1.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part2.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part3.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part4.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part5.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part6.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757052/ml-architect/ntm/naver-movie-review/train/train_part7.pbr
Uploaded data to s3://sagemaker-ap-northeast-2-057716757[...]<jupyter_text>--- Model TrainingWe have created the training and validation data sets and uploaded them to S3. Next, we configure a SageMaker training job to use the NTM algorithm on the data we prepared SageMaker uses Amazon Elastic Container Registry (ECR) docker container to host the NTM training image. The following ECR containers are currently available for SageMaker NTM training in different regions. For the latest Docker container registry please refer to [Amazon SageMaker: Common Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html).<jupyter_code>from sagemaker import image_uris, session
container = image_uris.retrieve("ntm", session.Session().boto_region_name, version="latest")<jupyter_output>Defaulting to the only supported framework/algorithm version: 1. Ignoring framework/algorithm version: latest.<jupyter_text>The code in the cell below automatically chooses an algorithm container based on the current region. In the API call to `sagemaker.estimator.Estimator` we also specify the type and count of instances for the training job. Because the 20NewsGroups data set is relatively small, we have chosen a CPU only instance (`ml.c4.xlarge`), but do feel free to change to [other instance types](https://aws.amazon.com/sagemaker/pricing/instance-types/). NTM fully takes advantage of GPU hardware and in general trains roughly an order of magnitude faster on a GPU than on a CPU. Multi-GPU or multi-instance training further improves training speed roughly linearly if communication overhead is low compared to compute time.<jupyter_code>import sagemaker
sess = sagemaker.Session()
ntm = sagemaker.estimator.Estimator(container,
                                    role, 
                                    instance_count=2, 
                                    instance_type='ml.c5.18xlarge',
                                    output_path=output_path,
                                    sagemaker_session=sess)<jupyter_output><empty_output><jupyter_text>HyperparametersHere we highlight a few hyperparameters. For information about the full list of available hyperparameters, please refer to [NTM Hyperparameters](https://docs.aws.amazon.com/sagemaker/latest/dg/ntm_hyperparameters.html).- **feature_dim** - the "feature dimension", it should be set to the vocabulary size- **num_topics** - the number of topics to extract- **mini_batch_size** - this is the batch size for each worker instance. Note that in multi-GPU instances, this number will be further divided by the number of GPUs. Therefore, for example, if we plan to train on an 8-GPU machine (such as `ml.p2.8xlarge`) and wish each GPU to have 1024 training examples per batch, `mini_batch_size` should be set to 8196.- **epochs** - the maximal number of epochs to train for, training may stop early- **num_patience_epochs** and **tolerance** controls the early stopping behavior. Roughly speaking, the algorithm will stop training if within the last `num_patience_epochs` epochs there have not been improvements on validation loss. Improvements smaller than `tolerance` will be considered non-improvement.- **optimizer** and **learning_rate** - by default we use `adadelta` optimizer and `learning_rate` does not need to be set. For other optimizers, the choice of an appropriate learning rate may require experimentation.<jupyter_code># num_topics = 20
num_topics = 5
ntm.set_hyperparameters(num_topics=num_topics, feature_dim=vocab_size, mini_batch_size=128, 
                        epochs=100, num_patience_epochs=5, tolerance=0.001)<jupyter_output><empty_output><jupyter_text>Next, we need to specify how the training data and validation data will be distributed to the workers during training. There are two modes for data channels:- `FullyReplicated`: all data files will be copied to all workers- `ShardedByS3Key`: data files will be sharded to different workers, i.e. each worker will receive a different portion of the full data set.At the time of writing, by default, the Python SDK will use `FullyReplicated` mode for all data channels. This is desirable for validation (test) channel but not suitable for training channel. The reason is that when we use multiple workers we would like to go through the full data set by each of them going through a different portion of the data set, so as to provide different gradients within epochs. Using `FullyReplicated` mode on training data not only results in slower training time per epoch (nearly 1.5X in this example), but also defeats the purpose of distributed training. To set the training data channel correctly we specify `distribution` to be `ShardedByS3Key` for the training data channel as follows.<jupyter_code># from sagemaker.session import s3_input
from sagemaker.inputs import TrainingInput
# s3_train = s3_input(s3_train_data, distribution='ShardedByS3Key') 
s3_train = TrainingInput(s3_train_data, distribution='ShardedByS3Key')<jupyter_output><empty_output><jupyter_text>Now we are ready to train. The following cell takes a few minutes to run. The command below will first provision the required hardware. You will see a series of dots indicating the progress of the hardware provisioning process. Once the resources are allocated, training logs will be displayed. With multiple workers, the log color and the ID following `INFO` identifies logs emitted by different workers.<jupyter_code>ntm.fit({'train': s3_train, 'validation': s3_val_data})<jupyter_output>2020-10-07 05:33:53 Starting - Starting the training job...
2020-10-07 05:34:02 Starting - Launching requested ML instances......
2020-10-07 05:35:04 Starting - Preparing the instances for training......
2020-10-07 05:36:01 Downloading - Downloading input data...
2020-10-07 05:36:48 Training - Training image download completed. Training in progress..[35mDocker entrypoint called with argument(s): train[0m
[35mRunning default environment configuration script[0m
[34mDocker entrypoint called with argument(s): train[0m
[34mRunning default environment configuration script[0m
[34m/opt/amazon/lib/python2.7/site-packages/pandas/util/nosetester.py:13: DeprecationWarning: Importing from numpy.testing.nosetester is deprecated, import from numpy.testing instead.
  from numpy.testing import nosetester[0m
[34m[10/07/2020 05:36:49 INFO 140554461984576] Reading default configuration from /opt/amazon/lib/python2.7/site-packages/algorithm/default-input.json: {u'num_patience_epochs': u'3', u'cl[...]<jupyter_text>If you see the message> `===== Job Complete =====`at the bottom of the output logs then that means training successfully completed and the output NTM model was stored in the specified output path. You can also view information about and the status of a training job using the AWS SageMaker console. Just click on the "Jobs" tab and select training job matching the training job name, below:<jupyter_code>print('Training job name: {}'.format(ntm.latest_training_job.job_name))<jupyter_output>Training job name: ntm-2020-10-07-05-33-53-240<jupyter_text>Model Hosting and InferenceA trained NTM model does nothing on its own. We now want to use the model we computed to perform inference on data. For this example, that means predicting the topic mixture representing a given document.We create an inference endpoint using the SageMaker Python SDK `deploy()` function from the job we defined above. We specify the instance type where inference is computed as well as an initial number of instances to spin up.<jupyter_code>%%time
ntm_predictor = ntm.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')<jupyter_output>-------------!<jupyter_text>Congratulations! You now have a functioning SageMaker NTM inference endpoint. You can confirm the endpoint configuration and status by navigating to the "Endpoints" tab in the AWS SageMaker console and selecting the endpoint matching the endpoint name, below:<jupyter_code>ntm_predictor.endpoint_name
print('Endpoint name: {}'.format(ntm_predictor.endpoint_name))<jupyter_output>Endpoint name: ntm-2020-10-07-05-39-03-921<jupyter_text>--- Data Serialization/DeserializationWe can pass data in a variety of formats to our inference endpoint. First, we will demonstrate passing CSV-formatted data. We make use of the SageMaker Python SDK utilities `csv_serializer` and `json_deserializer` when configuring the inference endpoint.  Inference with CSV<jupyter_code># from sagemaker.predictor import csv_serializer, json_deserializer
from sagemaker.deserializers import JSONDeserializer
from sagemaker.serializers import CSVSerializer

# ntm_predictor.content_type = 'text/csv'
ntm_predictor.serializer = CSVSerializer()
ntm_predictor.deserializer = JSONDeserializer()<jupyter_output><empty_output><jupyter_text>Let's pass 5 examples from the test set to the inference endpoint<jupyter_code>test_data = np.array(test_vectors.todense())
results = ntm_predictor.predict(test_data[:5])
print(results)<jupyter_output>{'predictions': [{'topic_weights': [0.1988115758, 0.2022137344, 0.198289156, 0.1970998645, 0.2035856843]}, {'topic_weights': [0.2021822631, 0.1999334544, 0.1977099329, 0.1975263804, 0.2026479691]}, {'topic_weights': [0.203534171, 0.199104473, 0.1974337399, 0.1976194084, 0.2023082078]}, {'topic_weights': [0.203706786, 0.1985468566, 0.197843343, 0.1979774833, 0.201925531]}, {'topic_weights': [0.1993484199, 0.2019989938, 0.1980570704, 0.1969904304, 0.2036051154]}]}<jupyter_text>We can see the output format of SageMaker NTM inference endpoint is a Python dictionary with the following format.```{  'predictions': [    {'topic_weights': [ ... ] },    {'topic_weights': [ ... ] },    {'topic_weights': [ ... ] },    ...  ]}```We extract the topic weights, themselves, corresponding to each of the input documents.<jupyter_code>predictions = np.array([prediction['topic_weights'] for prediction in results['predictions']])
print(predictions)<jupyter_output>[[0.19881158 0.20221373 0.19828916 0.19709986 0.20358568]
 [0.20218226 0.19993345 0.19770993 0.19752638 0.20264797]
 [0.20353417 0.19910447 0.19743374 0.19761941 0.20230821]
 [0.20370679 0.19854686 0.19784334 0.19797748 0.20192553]
 [0.19934842 0.20199899 0.19805707 0.19699043 0.20360512]]<jupyter_text>--- Inference with RecordIO ProtobufThe inference endpoint also supports JSON-formatted and RecordIO Protobuf, see [Common Data Formats—Inference](https://docs.aws.amazon.com/sagemaker/latest/dg/cdf-inference.html) for more information. At the time of writing SageMaker Python SDK does not yet have a RecordIO Protobuf serializer, but it is fairly straightforward to create one as follows.<jupyter_code># def recordio_protobuf_serializer(spmatrix):
#     import io
#     import sagemaker.amazon.common as smac
#     buf = io.BytesIO()
#     smac.write_spmatrix_to_sparse_tensor(array=spmatrix, file=buf, labels=None)
#     buf.seek(0)
#     return buf<jupyter_output><empty_output><jupyter_text>Now we specify the serializer to be the one we just crated and `content_type` to be 'application/x-recordio-protobuf' and inference can be carried out with RecordIO Protobuf format<jupyter_code># # ntm_predictor.content_type = 'application/x-recordio-protobuf'
# ntm_predictor.serializer = recordio_protobuf_serializer
# ntm_predictor.deserializer = JSONDeserializer()
# results = ntm_predictor.predict(test_vectors[:5])
# print(results)<jupyter_output><empty_output><jupyter_text>If you decide to compare these results to the known topic weights generated above keep in mind that SageMaker NTM discovers topics in no particular order. That is, the approximate topic mixtures computed above may be (approximate) permutations of the known topic mixtures corresponding to the same documents. ---Now we can take a look at how the 20 topics are assigned to the 5 test documents with a bar plot.<jupyter_code>import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

fs = 12
df=pd.DataFrame(predictions.T)
df.plot(kind='bar', figsize=(16,4), fontsize=fs)
plt.ylabel('Topic assignment', fontsize=fs+2)
plt.xlabel('Topic ID', fontsize=fs+2)<jupyter_output><empty_output><jupyter_text>Stop / Close the EndpointFinally, we should delete the endpoint before we close the notebook.To restart the endpoint you can follow the code above using the same `endpoint_name` we created or you can navigate to the "Endpoints" tab in the SageMaker console, select the endpoint with the name stored in the variable `endpoint_name`, and select "Delete" from the "Actions" dropdown menu.<jupyter_code># sagemaker.Session().delete_endpoint(ntm_predictor.endpoint)<jupyter_output><empty_output><jupyter_text>Model Exploration ***Note: The following section is meant as a deeper dive into exploring the trained models. The demonstrated functionalities may not be fully supported or guaranteed. For example, the parameter names may change without notice.***The trained model artifact is a compressed package of MXNet models from the two workers. To explore the model, we first need to install mxnet.<jupyter_code># If you use conda_mxnet_p36 kernel, mxnet is already installed, otherwise, uncomment the following line to install.
# !pip install mxnet 
import boto3
import mxnet as mx<jupyter_output><empty_output><jupyter_text>Here we download unpack the artifact<jupyter_code>model_path = os.path.join(output_prefix, ntm._current_job_name, 'output/model.tar.gz')
model_path
boto3.resource('s3').Bucket(bucket).download_file(model_path, 'downloaded_model.tar.gz')
!tar -xzvf 'downloaded_model.tar.gz'
# use flag -o to overwrite previous unzipped content
!unzip -o model_algo-1<jupyter_output>Archive:  model_algo-1
 extracting: meta.json               
 extracting: symbol.json             
 extracting: params<jupyter_text>We can load the model parameters and extract the weight matrix $W$ in the decoder as follows<jupyter_code>model = mx.ndarray.load('params')
W = model['arg:projection_weight']<jupyter_output><empty_output><jupyter_text>Matrix $W$ corresponds to the $W$ in the NTM digram at the beginning of this notebook. Each column of $W$ corresponds to a learned topic. The elements in the columns of $W$ corresponds to the pseudo-probability of a word within a topic. We can visualize each topic as a word cloud with the size of each word be proportional to the pseudo-probability of the words appearing under each topic.<jupyter_code># !pip install wordcloud
import wordcloud as wc
word_to_id = dict()
for i, v in enumerate(vocab_list):
    word_to_id[v] = i

limit = 24
n_col = 4
counter = 0

plt.figure(figsize=(20,16))
for ind in range(num_topics):

    if counter >= limit:
        break

    title_str = 'Topic{}'.format(ind)

    #pvals = mx.nd.softmax(W[:, ind]).asnumpy()
    pvals = mx.nd.softmax(mx.nd.array(W[:, ind])).asnumpy()

    word_freq = dict()
    for k in word_to_id.keys():
        i = word_to_id[k]
        word_freq[k] =pvals[i]

    # print(word_freq)
    font_path = 'NanumSquareRoundB.ttf'
    wordcloud = wc.WordCloud(background_color='white',font_path = font_path).fit_words(word_freq)

    plt.subplot(limit // n_col, n_col, counter+1)
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis("off")
    plt.title(title_str)
    #plt.close()

    counter +=1<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0893476268281991, 'fraction_numerical': 0.04168493205276372, 'mean_word_length': 4.871314927468414, 'pattern_counts': {'":': 0, '<': 99, '<?xml version=': 0, '>': 103, 'https://': 5, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '27', 'dir': 'jupyter-structured-clean-dedup', 'id': '521562', 'license': 'MIT', 'n_tokens_mistral': 8483, 'n_tokens_neox': 7743, 'n_words': 2574, 'path': 'NLP-Text-Class/working/ntm_naver_movie_topic_model.ipynb', 'repo_name': 'gonsoomoon-ml/topic-modeling', 'size': '25093'}