# coding: utf-8

# *Python Machine Learning 2nd Edition* by [Sebastian Raschka](https://sebastianraschka.com) and Vahid Mirjalili, Packt Publishing Ltd. 2017
#
# Code Repository: https://github.com/rasbt/python-machine-learning-book-2nd-edition
#
# Code License: [MIT License](https://github.com/rasbt/python-machine-learning-book-2nd-edition/blob/master/LICENSE.txt)

# # Python Machine Learning - Code Examples

# # Chapter 16 - Modeling Sequential Data Using Recurrent Neural Networks
#
#

# Note that the optional watermark extension is a small IPython notebook plugin that is being used to make the code reproducible. You can just skip the following line(s).

# In[1]:

# *The use of `watermark` is optional. You can install this IPython extension via "`pip install watermark`". For more information, please see: https://github.com/rasbt/watermark.*

# - [Introducing sequential data](#Introducing-sequential-data)
#   - [Modeling sequential data: Order matters](#Modeling-sequential-data:-Order-matters)
#   - [Understanding the different categories of sequence modeling](#Understanding-the-different-categories-of-sequence-modeling)
# - [Recurrent neural networks for modeling sequences](#Recurrent-neural-networks-for-modeling-sequences)
#   - [Understanding the structure and flow of a recurrent neural network
# ](#Understanding-the-structure-and-flow-of-a-recurrent-neural-network)
#   - [Computing activations in an RNN](#Computing-activations-in-an-RNN)
#   - [The challenges of learning long-range interactions](#The-challenges-of-learning-long-range-interactions)
#   - [Long short-term memory units](#Long-short-term-memory-units)
# - [Implementing a multilayer RNN for sequence modeling in TensorFlow](#Implementing-a-multilayer-RNN-for-sequence-modeling-in-TensorFlow)
#   - [Performing sentiment analysis of IMDb movie reviews using multilayer RNNs](#Performing-sentiment-analysis-of-IMDb-movie-reviews-using-multilayer-RNNs)
#     - [Preparing the data](#Preparing-the-data)
#     - [Embedding](#Embedding)
#     - [Building the RNN model](#Building-the-RNN-model)
#       - [Step 1: Defining multilayer RNN cells](#Step-1:-Defining-multilayer-RNN-cells)
#       - [Step 2: Defining the initial states for the RNN cells](#Step-2:-Defining-the-initial-states-for-the-RNN-cells)
#       - [Step 3: Creating the recurrent neural network using the RNN cells and their states](#Step-3:-Creating-the-recurrent-neural-network-using-the-RNN-cells-and-their-states)
#   - [Example application: character-level language modeling](#Example-application:-character-level-language-modeling)
#     - [Preparing the data](#Preparing-the-data)
#     - [Building the character-level RNN model](#Building-the-character-level-RNN-model)
# - [Summary](#Summary)

# In[2]:

from IPython.display import Image

# In[ ]:

import gzip

with gzip.open('movie_data.csv.gz') as f_in, open('movie_data.csv', 'wb') as f_out:
    f_out.writelines(f_in)

# # Introducing sequential data

# ## Modeling sequential data: Order matters

# ## Representing sequences

# In[3]:

# ## Understanding the different categories of sequence modeling

# In[4]:

# # Recurrent neural networks for modeling sequences

# ## Understanding the structure and flow of a recurrent neural network

# In[5]:

# In[6]:

# ## Computing activations in an RNN

# In[7]:

# In[8]:

# ## The challenges of learning long-range interactions

# In[9]:

# ## Long short-term memory units

# In[10]:

# # Implementing a multilayer RNN for sequence modeling in TensorFlow

# ## Performing sentiment analysis of IMDb movie reviews using multilayer RNNs

# ### Preparing the data

# In[11]:

# In[1]:

import pyprind
import pandas as pd
from string import punctuation
import re
import numpy as np

df = pd.read_csv('movie_data.csv', encoding='utf-8')
print(df.head(3))

# In[ ]:

## @Readers: PLEASE IGNORE THIS CELL
##
## This cell is meant to shrink the
## dataset when this notebook is run
## on the Travis Continuous Integration
## platform to test the code as well as
## speeding up the run using a smaller
## dataset for debugging

import os

if 'TRAVIS' in os.environ:
    df = pd.read_csv('movie_data.csv', encoding='utf-8', nrows=500)

# In[2]:

## Preprocessing the data:
## Separate words and
## count each word's occurrence

from collections import Counter

counts = Counter()
pbar = pyprind.ProgBar(len(df['review']),
                       title='Counting words occurences')
for i,review in enumerate(df['review']):
    text = ''.join([c if c not in punctuation else ' '+c+' '                     for c in review]).lower()
    df.loc[i,'review'] = text
    pbar.update()
    counts.update(text.split())

# In[3]:

## Create a mapping:
## Map each unique word to an integer

word_counts = sorted(counts, key=counts.get, reverse=True)
print(word_counts[:5])
word_to_int = {word: ii for ii, word in enumerate(word_counts, 1)}

mapped_reviews = []
pbar = pyprind.ProgBar(len(df['review']),
                       title='Map reviews to ints')
for review in df['review']:
    mapped_reviews.append([word_to_int[word] for word in review.split()])
    pbar.update()

# In[4]:

## Define fixed-length sequences:
## Use the last 200 elements of each sequence
## if sequence length < 200: left-pad with zeros

sequence_length = 200  ## sequence length (or T in our formulas)
sequences = np.zeros((len(mapped_reviews), sequence_length), dtype=int)
for i, row in enumerate(mapped_reviews):
    review_arr = np.array(row)
    sequences[i, -len(row):] = review_arr[-sequence_length:]

X_train = sequences[:25000, :]
y_train = df.loc[:25000, 'sentiment'].values
X_test = sequences[25000:, :]
y_test = df.loc[25000:, 'sentiment'].values

np.random.seed(123) # for reproducibility

## Function to generate minibatches:
def create_batch_generator(x, y=None, batch_size=64):
    n_batches = len(x)//batch_size
    x= x[:n_batches*batch_size]
    if y is not None:
        y = y[:n_batches*batch_size]
    for ii in range(0, len(x), batch_size):
        if y is not None:
            yield x[ii:ii+batch_size], y[ii:ii+batch_size]
        else:
            yield x[ii:ii+batch_size]

# In[ ]:

## @Readers: PLEASE IGNORE THIS CELL
##
## This cell is meant to shrink the
## dataset when this notebook is run
## on the Travis Continuous Integration
## platform to test the code as well as
## speeding up the run using a smaller
## dataset for debugging

if 'TRAVIS' in os.environ:
    X_train = sequences[:250, :]
    y_train = df.loc[:250, 'sentiment'].values
    X_test = sequences[250:500, :]
    y_test = df.loc[250:500, 'sentiment'].values

# ### Embedding

# In[12]:

# ### Building the RNN model

# In[5]:

import tensorflow as tf

class SentimentRNN(object):
    def __init__(self, n_words, seq_len=200,
                 lstm_size=256, num_layers=1, batch_size=64,
                 learning_rate=0.0001, embed_size=200):
        self.n_words = n_words
        self.seq_len = seq_len
        self.lstm_size = lstm_size   ## number of hidden units
        self.num_layers = num_layers
        self.batch_size = batch_size
        self.learning_rate = learning_rate
        self.embed_size = embed_size

        self.g = tf.Graph()
        with self.g.as_default():
            tf.set_random_seed(123)
            self.build()
            self.saver = tf.train.Saver()
            self.init_op = tf.global_variables_initializer()

    def build(self):
        ## Define the placeholders
        tf_x = tf.placeholder(tf.int32,
                    shape=(self.batch_size, self.seq_len),
                    name='tf_x')
        tf_y = tf.placeholder(tf.float32,
                    shape=(self.batch_size),
                    name='tf_y')
        tf_keepprob = tf.placeholder(tf.float32,
                    name='tf_keepprob')
        ## Create the embedding layer
        embedding = tf.Variable(
                    tf.random_uniform(
                        (self.n_words, self.embed_size),
                        minval=-1, maxval=1),
                    name='embedding')
        embed_x = tf.nn.embedding_lookup(
                    embedding, tf_x,
                    name='embeded_x')

        ## Define LSTM cell and stack them together
        cells = tf.contrib.rnn.MultiRNNCell(
                [tf.contrib.rnn.DropoutWrapper(
                   tf.contrib.rnn.BasicLSTMCell(self.lstm_size),
                   output_keep_prob=tf_keepprob)
                 for i in range(self.num_layers)])

        ## Define the initial state:
        self.initial_state = cells.zero_state(
                 self.batch_size, tf.float32)
        print('  << initial state >> ', self.initial_state)

        lstm_outputs, self.final_state = tf.nn.dynamic_rnn(
                 cells, embed_x,
                 initial_state=self.initial_state)
        ## Note: lstm_outputs shape:
        ##  [batch_size, max_time, cells.output_size]
        print('\n  << lstm_output   >> ', lstm_outputs)
        print('\n  << final state   >> ', self.final_state)

        ## Apply a FC layer after on top of RNN output:
        logits = tf.layers.dense(
                 inputs=lstm_outputs[:, -1],
                 units=1, activation=None,
                 name='logits')

        logits = tf.squeeze(logits, name='logits_squeezed')
        print ('\n  << logits        >> ', logits)

        y_proba = tf.nn.sigmoid(logits, name='probabilities')
        predictions = {
            'probabilities': y_proba,
            'labels' : tf.cast(tf.round(y_proba), tf.int32,
                 name='labels')
        }
        print('\n  << predictions   >> ', predictions)

        ## Define the cost function
        cost = tf.reduce_mean(
                 tf.nn.sigmoid_cross_entropy_with_logits(
                 labels=tf_y, logits=logits),
                 name='cost')

        ## Define the optimizer
        optimizer = tf.train.AdamOptimizer(self.learning_rate)
        train_op = optimizer.minimize(cost, name='train_op')

    def train(self, X_train, y_train, num_epochs):
        with tf.Session(graph=self.g) as sess:
            sess.run(self.init_op)
            iteration = 1
            for epoch in range(num_epochs):
                state = sess.run(self.initial_state)

                for batch_x, batch_y in create_batch_generator(
                            X_train, y_train, self.batch_size):
                    feed = {'tf_x:0': batch_x,
                            'tf_y:0': batch_y,
                            'tf_keepprob:0': 0.5,
                            self.initial_state : state}
                    loss, _, state = sess.run(
                            ['cost:0', 'train_op',
                             self.final_state],
                            feed_dict=feed)

                    if iteration % 20 == 0:
                        print("Epoch: %d/%d Iteration: %d "
                              "| Train loss: %.5f" % (
                               epoch + 1, num_epochs,
                               iteration, loss))

                    iteration +=1
                if (epoch+1)%10 == 0:
                    self.saver.save(sess,
                        "model/sentiment-%d.ckpt" % epoch)

    def predict(self, X_data, return_proba=False):
        preds = []
        with tf.Session(graph = self.g) as sess:
            self.saver.restore(
                sess, tf.train.latest_checkpoint('model/'))
            test_state = sess.run(self.initial_state)
            for ii, batch_x in enumerate(
                create_batch_generator(
                    X_data, None, batch_size=self.batch_size), 1):
                feed = {'tf_x:0' : batch_x,
                        'tf_keepprob:0': 1.0,
                        self.initial_state : test_state}
                if return_proba:
                    pred, test_state = sess.run(
                        ['probabilities:0', self.final_state],
                        feed_dict=feed)
                else:
                    pred, test_state = sess.run(
                        ['labels:0', self.final_state],
                        feed_dict=feed)

                preds.append(pred)

        return np.concatenate(preds)

# #### Step 1: Defining multilayer RNN cells

# #### Step 2: Defining the initial states for the RNN cells
#

# #### Step 3: Creating the recurrent neural network using the RNN cells and their states
#
#

# In[6]:

## Train:

n_words = max(list(word_to_int.values())) + 1

rnn = SentimentRNN(n_words=n_words,
                   seq_len=sequence_length,
                   embed_size=256,
                   lstm_size=128,
                   num_layers=1,
                   batch_size=100,
                   learning_rate=0.001)

# In[7]:

rnn.train(X_train, y_train, num_epochs=40)

# In[ ]:

## Test:
preds = rnn.predict(X_test)
y_true = y_test[:len(preds)]
print('Test Acc.: %.3f' % (
      np.sum(preds == y_true) / len(y_true)))

# In[ ]:

## Get probabilities:
proba = rnn.predict(X_test, return_proba=True)

# ## Example application: character-level language modeling

# In[13]:

# ### Preparing the data
#

# In[14]:

# In[15]:

# In[16]:

# In[1]:

import numpy as np

## Reading and processing text
with open('pg2265.txt', 'r', encoding='utf-8') as f:
    text=f.read()

text = text[15858:]
chars = set(text)
char2int = {ch:i for i,ch in enumerate(chars)}
int2char = dict(enumerate(chars))
text_ints = np.array([char2int[ch] for ch in text],
                     dtype=np.int32)

# In[ ]:

## @Readers: PLEASE IGNORE THIS CELL
##
## This cell is meant to shrink the
## dataset when this notebook is run
## on the Travis Continuous Integration
## platform to test the code as well as
## speeding up the run using a smaller
## dataset for debugging

if 'TRAVIS' in os.environ:
    text = text[:1000]
    chars = set(text)
    char2int = {ch:i for i,ch in enumerate(chars)}
    int2char = dict(enumerate(chars))
    text_ints = np.array([char2int[ch] for ch in text],
                         dtype=np.int32)

# In[2]:

def reshape_data(sequence, batch_size, num_steps):
    tot_batch_length = batch_size * num_steps
    num_batches = int(len(sequence) / tot_batch_length)
    if num_batches*tot_batch_length + 1 > len(sequence):
        num_batches = num_batches - 1
    ## Truncate the sequence at the end to get rid of
    ## remaining charcaters that do not make a full batch
    x = sequence[0 : num_batches*tot_batch_length]
    y = sequence[1 : num_batches*tot_batch_length + 1]
    ## Split x & y into a list batches of sequences:
    x_batch_splits = np.split(x, batch_size)
    y_batch_splits = np.split(y, batch_size)
    ## Stack the batches together
    ## batch_size x tot_batch_length
    x = np.stack(x_batch_splits)
    y = np.stack(y_batch_splits)

    return x, y

## Testing:
train_x, train_y = reshape_data(text_ints, 64, 10)
print(train_x.shape)
print(train_x[0, :10])
print(train_y[0, :10])
print(''.join(int2char[i] for i in train_x[0, :50]))

# In[3]:

np.random.seed(123)

def create_batch_generator(data_x, data_y, num_steps):
    batch_size, tot_batch_length = data_x.shape
    num_batches = int(tot_batch_length/num_steps)
    for b in range(num_batches):
        yield (data_x[:, b*num_steps: (b+1)*num_steps],
               data_y[:, b*num_steps: (b+1)*num_steps])

bgen = create_batch_generator(train_x[:,:100], train_y[:,:100], 15)
for b in bgen:
    print(b[0].shape, b[1].shape, end='  ')
    print(''.join(int2char[i] for i in b[0][0,:]).replace('\n', '*'), '    ',
          ''.join(int2char[i] for i in b[1][0,:]).replace('\n', '*'))

# ### Building the character-level RNN model

# In[4]:

import tensorflow as tf
import os

class CharRNN(object):
    def __init__(self, num_classes, batch_size=64,
                 num_steps=100, lstm_size=128,
                 num_layers=1, learning_rate=0.001,
                 keep_prob=0.5, grad_clip=5,
                 sampling=False):
        self.num_classes = num_classes
        self.batch_size = batch_size
        self.num_steps = num_steps
        self.lstm_size = lstm_size
        self.num_layers = num_layers
        self.learning_rate = learning_rate
        self.keep_prob = keep_prob
        self.grad_clip = grad_clip

        self.g = tf.Graph()
        with self.g.as_default():
            tf.set_random_seed(123)

            self.build(sampling=sampling)
            self.saver = tf.train.Saver()
            self.init_op = tf.global_variables_initializer()

    def build(self, sampling):
        if sampling == True:
            batch_size, num_steps = 1, 1
        else:
            batch_size = self.batch_size
            num_steps = self.num_steps

        tf_x = tf.placeholder(tf.int32,
                              shape=[batch_size, num_steps],
                              name='tf_x')
        tf_y = tf.placeholder(tf.int32,
                              shape=[batch_size, num_steps],
                              name='tf_y')
        tf_keepprob = tf.placeholder(tf.float32,
                              name='tf_keepprob')

        # One-hot encoding:
        x_onehot = tf.one_hot(tf_x, depth=self.num_classes)
        y_onehot = tf.one_hot(tf_y, depth=self.num_classes)

        ### Build the multi-layer RNN cells
        cells = tf.contrib.rnn.MultiRNNCell(
            [tf.contrib.rnn.DropoutWrapper(
                tf.contrib.rnn.BasicLSTMCell(self.lstm_size),
                output_keep_prob=tf_keepprob)
            for _ in range(self.num_layers)])

        ## Define the initial state
        self.initial_state = cells.zero_state(
                    batch_size, tf.float32)

        ## Run each sequence step through the RNN
        lstm_outputs, self.final_state = tf.nn.dynamic_rnn(
                    cells, x_onehot,
                    initial_state=self.initial_state)

        print('  << lstm_outputs  >>', lstm_outputs)

        seq_output_reshaped = tf.reshape(
                    lstm_outputs,
                    shape=[-1, self.lstm_size],
                    name='seq_output_reshaped')

        logits = tf.layers.dense(
                    inputs=seq_output_reshaped,
                    units=self.num_classes,
                    activation=None,
                    name='logits')

        proba = tf.nn.softmax(
                    logits,
                    name='probabilities')
        print(proba)

        y_reshaped = tf.reshape(
                    y_onehot,
                    shape=[-1, self.num_classes],
                    name='y_reshaped')
        cost = tf.reduce_mean(
                    tf.nn.softmax_cross_entropy_with_logits(
                        logits=logits,
                        labels=y_reshaped),
                    name='cost')

        # Gradient clipping to avoid "exploding gradients"
        tvars = tf.trainable_variables()
        grads, _ = tf.clip_by_global_norm(
                    tf.gradients(cost, tvars),
                    self.grad_clip)
        optimizer = tf.train.AdamOptimizer(self.learning_rate)
        train_op = optimizer.apply_gradients(
                    zip(grads, tvars),
                    name='train_op')

    def train(self, train_x, train_y,
              num_epochs, ckpt_dir='./model/'):
        ## Create the checkpoint directory
        ## if does not exists
        if not os.path.exists(ckpt_dir):
            os.mkdir(ckpt_dir)

        with tf.Session(graph=self.g) as sess:
            sess.run(self.init_op)

            n_batches = int(train_x.shape[1]/self.num_steps)
            iterations = n_batches * num_epochs
            for epoch in range(num_epochs):

                # Train network
                new_state = sess.run(self.initial_state)
                loss = 0
                ## Minibatch generator:
                bgen = create_batch_generator(
                        train_x, train_y, self.num_steps)
                for b, (batch_x, batch_y) in enumerate(bgen, 1):
                    iteration = epoch*n_batches + b

                    feed = {'tf_x:0': batch_x,
                            'tf_y:0': batch_y,
                            'tf_keepprob:0': self.keep_prob,
                            self.initial_state : new_state}
                    batch_cost, _, new_state = sess.run(
                            ['cost:0', 'train_op',
                                self.final_state],
                            feed_dict=feed)
                    if iteration % 10 == 0:
                        print('Epoch %d/%d Iteration %d'
                              '| Training loss: %.4f' % (
                              epoch + 1, num_epochs,
                              iteration, batch_cost))

                ## Save the trained model
                self.saver.save(
                        sess, os.path.join(
                            ckpt_dir, 'language_modeling.ckpt'))

    def sample(self, output_length,
               ckpt_dir, starter_seq="The "):
        observed_seq = [ch for ch in starter_seq]
        with tf.Session(graph=self.g) as sess:
            self.saver.restore(
                sess,
                tf.train.latest_checkpoint(ckpt_dir))
            ## 1: run the model using the starter sequence
            new_state = sess.run(self.initial_state)
            for ch in starter_seq:
                x = np.zeros((1, 1))
                x[0,0] = char2int[ch]
                feed = {'tf_x:0': x,
                        'tf_keepprob:0': 1.0,
                        self.initial_state: new_state}
                proba, new_state = sess.run(
                        ['probabilities:0', self.final_state],
                        feed_dict=feed)

            ch_id = get_top_char(proba, len(chars))
            observed_seq.append(int2char[ch_id])

            ## 2: run the model using the updated observed_seq
            for i in range(output_length):
                x[0,0] = ch_id
                feed = {'tf_x:0': x,
                        'tf_keepprob:0': 1.0,
                        self.initial_state: new_state}
                proba, new_state = sess.run(
                        ['probabilities:0', self.final_state],
                        feed_dict=feed)

                ch_id = get_top_char(proba, len(chars))
                observed_seq.append(int2char[ch_id])

        return ''.join(observed_seq)

# In[5]:

def get_top_char(probas, char_size, top_n=5):
    p = np.squeeze(probas)
    p[np.argsort(p)[:-top_n]] = 0.0
    p = p / np.sum(p)
    ch_id = np.random.choice(char_size, 1, p=p)[0]
    return ch_id

# In[6]:

batch_size = 64
num_steps = 100
train_x, train_y = reshape_data(text_ints,
                                batch_size,
                                num_steps)

rnn = CharRNN(num_classes=len(chars), batch_size=batch_size)
rnn.train(train_x, train_y,
          num_epochs=100,
          ckpt_dir='./model-100/')

# In[7]:

np.random.seed(123)
rnn = CharRNN(len(chars), sampling=True)

print(rnn.sample(ckpt_dir='./model-100/',
                 output_length=500))

# In[8]:

## run for 200 epochs
batch_size = 64
num_steps = 100

rnn = CharRNN(num_classes=len(chars), batch_size=batch_size)
rnn.train(train_x, train_y,
          num_epochs=200,
          ckpt_dir='./model-200/')

# In[9]:

del rnn

np.random.seed(123)
rnn = CharRNN(len(chars), sampling=True)
print(rnn.sample(ckpt_dir='./model-200/',
                 output_length=500))

# # Summary

# ...

# ---
#
# Readers may ignore the next cell.

# In[ ]:


"""
Introduction to Radar Course

Authors
=======

Zachary Chance, Robert Freking, Victoria Helus
MIT Lincoln Laboratory
Lexington, MA 02421

Distribution Statement
======================

DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.

This material is based upon work supported by the United States Air Force under Air
Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or
recommendations expressed in this material are those of the author(s) and do not
necessarily reflect the views of the United States Air Force.

© 2021 Massachusetts Institute of Technology.

The software/firmware is provided to you on an As-Is basis

Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part
252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S.
Government rights in this work are defined by DFARS 252.227-7013 or
DFARS 252.227-7014 as detailed above. Use of this work other than as specifically
authorized by the U.S. Government may violate any copyrights that exist in this work.

RAMS ID: 1016938
"""

from IPython.display import display
import ipywidgets as wdg
import math
import numpy as np
from rad.const import k, c
import rad.radar as rd

# jupyter_intro

# Simple quiz with scalar value
def new_quiz(prompt, val, abs_tol=None, rel_tol=1E-2):
    """
    Generate quiz answer box.

    Inputs:
    - prompt [str]: Question prompt
    - val [float]: True answer
    - abs_tol [float]: Absolute tolerance; if None, uses rel_tol
    - rel_tol [float]: Relative tolerance

    Outputs:
    (none)
    """
    title = wdg.HTML(value = f"
{prompt}
")
    answer = wdg.FloatText()
    submit = wdg.Button(description="Submit")
    result = wdg.HTML(value = f"Ready")
    ansbox = wdg.VBox([wdg.HBox([title, answer, result]), submit])
    display(ansbox)

    def check_ans(b):
        if abs_tol and (abs(answer.value - val) < abs_tol):
            result.value = f"Correct!"
        elif rel_tol and ((abs(answer.value - val)/abs(val)) < rel_tol):
            result.value = f"Correct!"
        else:
            result.value = f"Incorrect."

    submit.on_click(check_ans)

#-------Lab 1.1: Introduction to Labs-------

# Q1.1.1
def quiz_1_1_1():
    prompt = 'Enter answer:'
    val = 1.0
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.2
def quiz_1_1_2():
    prompt = 'Enter answer:'
    val = (math.log10(2.72**3) + math.cos(4*math.pi/7))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.3
def quiz_1_1_3():
    prompt = 'Enter answer:'
    val = 7.76E4/5.1E-3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.4a
def quiz_1_1_4a():
    prompt = 'Enter answer (in dB):'
    val = rd.to_db(1.5E5)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.4b
def quiz_1_1_4b():
    prompt = 'Enter answer (in dB):'
    val = rd.to_db(7.2E-7)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.5a
def quiz_1_1_5a():
    prompt = 'Enter answer:'
    val = rd.from_db(51.2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.5b
def quiz_1_1_5b():
    prompt = 'Enter answer:'
    val = rd.from_db(-20.1)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.6
def quiz_1_1_6():
    prompt = 'Enter answer (in dB):'
    val = rd.to_db(3.7**5)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.7a
def quiz_1_1_7a():
    prompt = 'Enter answer:'
    val = 3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.1.7b
def quiz_1_1_7b():
    prompt = 'Enter answer (in s, within ±0.05 s):'
    val = 0.5
    tol = 0.05
    new_quiz(prompt, val, abs_tol=tol)

# Q1.1.7c
def quiz_1_1_7c():
    prompt = 'Enter answer (in Hz, within ±0.2 Hz):'
    val = 2
    tol = 0.2
    new_quiz(prompt, val, abs_tol=tol)

#-------Lab 1.2: Introduction to Radar-------

# Q1.2.1a
def quiz_1_2_1a():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(1000, propvel=1000)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.1b
def quiz_1_2_1b():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(1000, propvel=2000)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.1c
def quiz_1_2_1c():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(500, propvel=1000)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.2a
def quiz_1_2_2a():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(500E6, propvel=3E8)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.2b
def quiz_1_2_2b():
    prompt = 'Enter frequency (in Hz):'
    val = 1500/5
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.3a
def quiz_1_2_3a():
    prompt = 'Enter time (in ms):'
    val = math.sqrt(75**2 + 80**2)
    tol = 3
    new_quiz(prompt, val, abs_tol=tol)

# Q1.2.3b
def quiz_1_2_3b():
    prompt = 'Enter time (in ms):'
    val = 2*math.sqrt(75**2 + 80**2)
    tol = 3
    new_quiz(prompt, val, abs_tol=tol)

# Q1.2.4a
def quiz_1_2_4a():
    prompt = 'Enter time (in s):'
    val = 2*1000E3/3E8
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.4b
def quiz_1_2_4b():
    prompt = 'Enter range (in m):'
    val = 3E8*0.0057/2
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.5
def quiz_1_2_5():
    prompt = 'Enter range (in m):'
    val = math.sqrt(75**2 + 100**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.6a
def quiz_1_2_6a():
    prompt = 'Enter range (in m):'
    val = math.sqrt(50**2 + 50**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.6b
def quiz_1_2_6b():
    prompt = 'Enter azimuth (in deg, within +/- 3 deg):'
    val = 90 - rd.rad2deg(math.atan2(50, 50))
    tol = 3
    new_quiz(prompt, val, abs_tol=tol)

# Q1.2.7a
def quiz_1_2_7a():
    prompt = 'Enter range (in m):'
    val = math.sqrt(40**2 + 20**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.7b
def quiz_1_2_7b():
    prompt = 'Enter azimuth (in deg, within +/- 3 deg):'
    val = 90 - rd.rad2deg(math.atan2(-20, 40))
    tol = 3
    new_quiz(prompt, val, abs_tol=tol)

# Q1.2.8a
def quiz_1_2_8a():
    prompt = 'Enter beamwidth (in deg):'
    val = 70*0.12/3.8
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.8b
def quiz_1_2_8b():
    prompt = 'Enter diameter (in m):'
    val = 70*rd.wavelen(10E9, propvel=3E8)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.9
def quiz_1_2_9():
    prompt = 'Enter transmit gain (in dB):'
    val = rd.to_db(4*math.pi*2.1*3.3/rd.wavelen(15E3, propvel=2E3)**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.10a
def quiz_1_2_10a():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(5E9)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.10b
def quiz_1_2_10b():
    prompt = 'Enter beamwidth (in deg):'
    val = 70*rd.wavelen(5E9)/4.4
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q1.2.10c
def quiz_1_2_10c():
    prompt = 'Enter transmit gain (in dB):'
    val = rd.to_db(4*math.pi*(math.pi*2.2**2)/rd.wavelen(5E9)**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

#-------Lab 2.1: Radar Range Equation-------

# Q2.1.1a
def quiz_2_1_1a():
    prompt = 'Enter SNR (in dB):'
    val = rd.to_db(100/5.3)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.1b
def quiz_2_1_1b():
    prompt = 'Enter signal energy (in J):'
    val = rd.from_db(15)*2.2
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.2a
def quiz_2_1_2a():
    prompt = 'Enter received power (in W):'
    val = rd.friis(50E3, 10E3, area=10, gain=rd.from_db(35.0))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.2b
def quiz_2_1_2b():
    prompt = 'Enter dish radius (in m):'
    val = np.sqrt(rd.gain2area((1/100E3/5)*4*np.pi*(10E3)**2, 3.5E9)/np.pi)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.3a
def quiz_2_1_3a():
    prompt = 'Enter incident power (in W):'
    val = rd.friis(50E3, 150E3, area=rd.from_db(5), gain=rd.from_db(30))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.3b
def quiz_2_1_3b():
    prompt = 'Enter range (in m):'
    val = math.sqrt(500E3*1*rd.from_db(25)/4/math.pi/0.1E-3)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.4a
def quiz_2_1_4a():
    prompt = 'Enter received power (in W):'
    val = rd.rx_power(100E3, 600E3, 3E9, rcs=rd.from_db(5), gain=rd.from_db(80))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.4b
def quiz_2_1_4b():
    prompt = 'Enter effective aperture area (in m^2):'
    val = rd.gain2area(rd.from_db(24.3), 1.3E9)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.4c
def quiz_2_1_4c():
    prompt = 'Enter RCS (in dBsm):'
    wlen = rd.wavelen(1.5E9)
    val = rd.to_db(10E-12*(10E3**4)*(4*np.pi)**3/30/rd.from_db(80)/wlen**2)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.5
def quiz_2_1_5():
    prompt = 'Enter noise energy (in J):'
    val = k*722
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.6
def quiz_2_1_6():
    prompt = 'Enter SNR (in dB):'
    val = rd.to_db(rd.rx_snr(200E3, 50, 5E9, 750, rcs=rd.from_db(-5), gain=rd.from_db(70)))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q2.1.7
def quiz_2_1_7():
    prompt = 'Enter SNR (in dB):'
    val = rd.to_db(rd.snrconv(rd.from_db(17), 75E3, rd.from_db(-10), 50E3, rd.from_db(-15)))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

#-------Lab 2.2: Basic Radar Design-------

#-------Lab 3.1: Radar Transmissions and Receptions-------

# Q3.1.1
def quiz_3_1_1():
    prompt = 'Enter pulsewidth (in µs):'
    val = 7.2
    tol = 0.1
    new_quiz(prompt, val, abs_tol=tol)

# Q3.1.2a
def quiz_3_1_2a():
    prompt = 'Enter PRI (in s):'
    val = 1/10E3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q3.1.2b
def quiz_3_1_2b():
    prompt = 'Enter duty cycle:'
    val = 150E-6*1E3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q3.1.3a
def quiz_3_1_3a():
    prompt = 'Enter delay (in ns):'
    val = -2.83
    tol = 0.01
    new_quiz(prompt, val, abs_tol=tol)

# Q3.1.3b
def quiz_3_1_3b():
    prompt = 'Enter delay (in ns):'
    val = -2.83
    tol = 0.01
    new_quiz(prompt, val, abs_tol=tol)

# Q3.1.4a
def quiz_3_1_4a():
    prompt = 'Enter phase (in deg):'
    val = 168
    tol = 1
    new_quiz(prompt, val, abs_tol=tol)

# Q3.1.4b
def quiz_3_1_4b():
    prompt = 'Enter phase (in deg):'
    val = 120
    tol = 1
    new_quiz(prompt, val, abs_tol=tol)

# Q3.1.5
def quiz_3_1_5():
    prompt = 'Enter bandwidth (in Hz):'
    val = c/2/10
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

#-------Lab 3.2: Detection-------

# Q3.2.1a
def quiz_3_2_1a():
    prompt = 'Enter prob. of detection:'
    val = 0.69
    tol = 0.02
    new_quiz(prompt, val, abs_tol=tol)

# Q3.2.1b
def quiz_3_2_1b():
    prompt = 'Enter prob. of false alarm:'
    val = 0.17
    tol = 0.02
    new_quiz(prompt, val, abs_tol=tol)

#-------Lab 4.1: Target Parameter Estimation-------

# Q4.1.1a
def quiz_4_1_1a():
    prompt = 'Enter range (in m):'
    val = c*0.00333/2
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.1b
def quiz_4_1_1b():
    prompt = 'Enter range resolution (in m):'
    val = rd.range_res(30E6)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.1c
def quiz_4_1_1c():
    prompt = 'Enter range accuracy (in m):'
    val = rd.range_res(30E6)/np.sqrt(rd.from_db(11))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.2a
def quiz_4_1_2a():
    prompt = 'Enter angle accuracy (in deg):'
    val = rd.dish_beamw(2, 3.5E9)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.2b
def quiz_4_1_2b():
    prompt = 'Enter angle accuracy (in deg):'
    val = rd.dish_beamw(2, 3.5E9)/np.sqrt(rd.from_db(14))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.3
def quiz_4_1_3():
    prompt = 'Enter cross-range resolution (in m):'
    val = 50E3*rd.wavelen(3.5E9)/2.1
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.4a
def quiz_4_1_4a():
    prompt = 'Enter Doppler shift (in Hz):'
    val = rd.dopp_shift(500, 1.5E9)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.4b
def quiz_4_1_4b():
    prompt = 'Enter range rate (in m/s):'
    val = -c*10E3/2/3.0E9
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.5
def quiz_4_1_5():
    prompt = 'Enter range rate (in m/s):'
    val = 55.0
    tol = 1.0
    new_quiz(prompt, val, abs_tol=tol)

# Q4.1.6a
def quiz_4_1_6a():
    prompt = 'Enter range rate resolution (in m/s):'
    val = c/2/5E9/50E-3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.6b
def quiz_4_1_6b():
    prompt = 'Enter range rate (in m/s):'
    val = c*500/4/5E9
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.7a
def quiz_4_1_7a():
    prompt = 'Enter RCS (in dBsm):'
    val = rd.to_db(rd.from_db(0)*(rd.from_db(13)/rd.from_db(15))*(65E3/50E3)**4)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q4.1.7b
def quiz_4_1_7b():
    prompt = 'Enter RCS (in dBsm):'
    val = rd.to_db(rd.from_db(5)*(rd.from_db(13)/rd.from_db(7))*(100E3/500E3)**4)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.1a
def quiz_5_1_1a():
    prompt = 'Enter wavelength (in m):'
    val = rd.wavelen(10E9)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.1b
def quiz_5_1_1b():
    prompt = 'Enter beamwidth (in deg):'
    val = 70*rd.wavelen(10E9)/4
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.1c
def quiz_5_1_1c():
    prompt = 'Enter transmit gain (in dB):'
    val = rd.to_db(rd.dish_gain(2, 10E9))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.2a
def quiz_5_1_2a():
    prompt = 'Enter received energy (in J):'
    val = rd.rx_energy(200E3, 10, 2E9, gain=rd.area2gain(10, 2E9)**2, rcs=rd.from_db(-5))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.2b
def quiz_5_1_2b():
    prompt = 'Enter SNR (in dB):'
    val = rd.to_db(rd.rx_energy(200E3, 10, 2E9, gain=rd.area2gain(10, 2E9)**2, rcs=rd.from_db(-5))) - \
        rd.to_db(k*500)
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.3a
def quiz_5_1_3a():
    prompt = 'Enter duty cycle:'
    val = 100E-6*2E3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.3b
def quiz_5_1_3b():
    prompt = 'Enter number of pulses:'
    val = math.ceil(rd.from_db(12))
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.4a
def quiz_5_1_4a():
    prompt = 'Enter bandwidth (in Hz):'
    val = c/2/1.5
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.4b
def quiz_5_1_4b():
    prompt = 'Enter cross range resolution (in m):'
    beamw = 57.3*rd.wavelen(5E9)/5.2
    val = 90E3*beamw/57.3
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.4c
def quiz_5_1_4c():
    prompt = 'Enter range rate (in m/s):'
    val = c*700/4/3E9
    tol = 0.01
    new_quiz(prompt, val, rel_tol=tol)

# Q5.1.5
def quiz_5_1_5(targets):
    for ii in range(len(targets)):
        prompt = f'Enter route number for target with RCS {targets[ii].rcs:.2f} dBsm:'
        val = targets[ii].route
        tol = 0.01
        new_quiz(prompt, val, abs_tol=tol)


# Importing packages and loading in the data set
from utils_pos import get_word_tag, preprocess
import pandas as pd
from collections import defaultdict
import math
import numpy as np

# load in the training corpus
with open("WSJ_02-21.pos", 'r') as f:
    training_corpus = f.readlines()

print(f"A few items of the training corpus list")
print(training_corpus[0:5])

# read the vocabulary data, split by each line of text, and save the list
with open("hmm_vocab.txt", 'r') as f:
    voc_l = f.read().split('\n')

print("A few items of the vocabulary list")
print(voc_l[0:50])
print()
print("A few items at the end of the vocabulary list")
print(voc_l[-50:])

# vocab: dictionary that has the index of the corresponding words
vocab = {}

# Get the index of the corresponding words.
for i, word in enumerate(sorted(voc_l)):
    vocab[word] = i

print("Vocabulary dictionary, key is the word, value is a unique integer")
cnt = 0
for k,v in vocab.items():
    print(f"{k}:{v}")
    cnt += 1
    if cnt > 20:
        break

# load in the test corpus
with open("WSJ_24.pos", 'r') as f:
    y = f.readlines()

print("A sample of the test corpus")
print(y[0:10])

#corpus without tags, preprocessed
_, prep = preprocess(vocab, "test.words")

print('The length of the preprocessed test corpus: ', len(prep))
print('This is a sample of the test_corpus: ')
print(prep[0:10])

# Part 1: Parts-of-speech tagging
# UNQ_C1
def create_dictionaries(training_corpus, vocab):
    """
    Input:
        training_corpus: a corpus where each line has a word followed by its tag.
        vocab: a dictionary where keys are words in vocabulary and value is an index
    Output:
        emission_counts: a dictionary where the keys are (tag, word) and the values are the counts
        transition_counts: a dictionary where the keys are (prev_tag, tag) and the values are the counts
        tag_counts: a dictionary where the keys are the tags and the values are the counts
    """

    # initialize the dictionaries using defaultdict
    emission_counts = defaultdict(int)
    transition_counts = defaultdict(int)
    tag_counts = defaultdict(int)

    # Initialize "prev_tag" (previous tag) with the start state, denoted by '--s--'
    prev_tag = '--s--'

    # use 'i' to track the line number in the corpus
    i = 0

    # Each item in the training corpus contains a word and its POS tag
    # Go through each word and its tag in the training corpus
    for word_tag in training_corpus:

        # Increment the word_tag count
        i += 1

        # Every 50,000 words, print the word count
        if i % 50000 == 0:
            print(f"word count = {i}")

        # get the word and tag using the get_word_tag helper function (imported from utils_pos.py)
        word, tag = get_word_tag(word_tag,vocab)

        # Increment the transition count for the previous word and tag
        transition_counts[(prev_tag, tag)] += 1

        # Increment the emission count for the tag and word
        emission_counts[(tag, word)] += 1

        # Increment the tag count
        tag_counts[tag] += 1

        # Set the previous tag to this tag (for the next iteration of the loop)
        prev_tag = tag

    return emission_counts, transition_counts, tag_counts

  emission_counts, transition_counts, tag_counts = create_dictionaries(training_corpus, vocab)

  # get all the POS states
states = sorted(tag_counts.keys())
print(f"Number of POS tags (number of 'states'): {len(states)}")
print("View these POS tags (states)")
print(states)

print("transition examples: ")
for ex in list(transition_counts.items())[:3]:
    print(ex)
print()

print("emission examples: ")
for ex in list(emission_counts.items())[200:203]:
    print (ex)
print()

print("ambiguous word example: ")
for tup,cnt in emission_counts.items():
    if tup[1] == 'back': print (tup, cnt)

# UNQ_C2 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: predict_pos

def predict_pos(prep, y, emission_counts, vocab, states):
    '''
    Input:
        prep: a preprocessed version of 'y'. A list with the 'word' component of the tuples.
        y: a corpus composed of a list of tuples where each tuple consists of (word, POS)
        emission_counts: a dictionary where the keys are (tag,word) tuples and the value is the count
        vocab: a dictionary where keys are words in vocabulary and value is an index
        states: a sorted list of all possible tags for this assignment
    Output:
        accuracy: Number of times you classified a word correctly
    '''

    # Initialize the number of correct predictions to zero
    num_correct = 0

    # Get the (tag, word) tuples, stored as a set
    all_words = set(emission_counts.keys())

    # Get the number of (word, POS) tuples in the corpus 'y'
    total = len(y)
    for word, y_tup in zip(prep, y):

        # Split the (word, POS) string into a list of two items
        y_tup_l = y_tup.split()

        # Verify that y_tup contain both word and POS
        if len(y_tup_l) == 2:

            # Set the true POS label for this word
            true_label = y_tup_l[1]

        else:
            # If the y_tup didn't contain word and POS, go to next word
            continue

        count_final = 0
        pos_final = ''

        # If the word is in the vocabulary...
        if word in vocab:
            for pos in states:

            ### START CODE HERE (Replace instances of 'None' with your code) ###

                # define the key as the tuple containing the POS and word
                key = (pos, word)

                # check if the (pos, word) key exists in the emission_counts dictionary
                if key in emission_counts.keys(): # complete this line

                # get the emission count of the (pos,word) tuple
                    count = emission_counts.get(key,0)

                    # keep track of the POS with the largest count
                    if count>count_final: # complete this line

                        # update the final count (largest count)
                        count_final = count

                        # update the final POS
                        pos_final = pos

            # If the final POS (with the largest count) matches the true POS:
            if pos_final==true_label: # complete this line

                # Update the number of correct predictions
                num_correct += 1

    ### END CODE HERE ###
    accuracy = num_correct / total

    return accuracy

accuracy_predict_pos = predict_pos(prep, y, emission_counts, vocab, states)
print(f"Accuracy of prediction using predict_pos is {accuracy_predict_pos:.4f}")

# UNQ_C3
def create_transition_matrix(alpha, tag_counts, transition_counts):
    '''
    Input:
        alpha: number used for smoothing
        tag_counts: a dictionary mapping each tag to its respective count
        transition_counts: transition count for the previous word and tag
    Output:
        A: matrix of dimension (num_tags,num_tags)
    '''
    # Get a sorted list of unique POS tags
    all_tags = sorted(tag_counts.keys())

    # Count the number of unique POS tags
    num_tags = len(all_tags)

    # Initialize the transition matrix 'A'
    A = np.zeros((num_tags,num_tags))

    # Get the unique transition tuples (previous POS, current POS)
    trans_keys = set(transition_counts.keys())

    # Go through each row of the transition matrix A
    for i in range(num_tags):

        # Go through each column of the transition matrix A
        for j in range(num_tags):

            # Initialize the count of the (prev POS, current POS) to zero
            count = 0

            # Define the tuple (prev POS, current POS)
            # Get the tag at position i and tag at position j (from the all_tags list)
            key = (all_tags[i], all_tags[j])

            # Check if the (prev POS, current POS) tuple
            # exists in the transition counts dictionary
            if key in trans_keys: #complete this line

                # Get count from the transition_counts dictionary
                # for the (prev POS, current POS) tuple
                count = transition_counts.get(key,0)

            # Get the count of the previous tag (index position i) from tag_counts
            count_prev_tag = tag_counts.get(all_tags[i],0)

            # Apply smoothing using count of the tuple, alpha,
            # count of previous tag, alpha, and total number of tags
            A[i,j] = (count + alpha)/(count_prev_tag+num_tags*alpha)

    return A

alpha = 0.001
A = create_transition_matrix(alpha, tag_counts, transition_counts)
# Testing your function
print(f"A at row 0, col 0: {A[0,0]:.9f}")
print(f"A at row 3, col 1: {A[3,1]:.4f}")

print("View a subset of transition matrix A")
A_sub = pd.DataFrame(A[30:35,30:35], index=states[30:35], columns = states[30:35] )
print(A_sub)

# UNQ_C4
def create_emission_matrix(alpha, tag_counts, emission_counts, vocab):
    '''
    Input:
        alpha: tuning parameter used in smoothing
        tag_counts: a dictionary mapping each tag to its respective count
        emission_counts: a dictionary where the keys are (tag, word) and the values are the counts
        vocab: a dictionary where keys are words in vocabulary and value is an index.
               within the function it'll be treated as a list
    Output:
        B: a matrix of dimension (num_tags, len(vocab))
    '''

    # get the number of POS tag
    num_tags = len(tag_counts)

    # Get a list of all POS tags
    all_tags = sorted(tag_counts.keys())

    # Get the total number of unique words in the vocabulary
    num_words = len(vocab)

    # Initialize the emission matrix B with places for
    # tags in the rows and words in the columns
    B = np.zeros((num_tags, num_words))

    # Get a set of all (POS, word) tuples
    # from the keys of the emission_counts dictionary
    emis_keys = set(list(emission_counts.keys()))

    # Go through each row (POS tags)
    for i in range(num_tags): # complete this line

        # Go through each column (words)
        for j in range(num_words): # complete this line

            # Initialize the emission count for the (POS tag, word) to zero
            count = 0

            # Define the (POS tag, word) tuple for this row and column
            key =  (all_tags[i], vocab[j])

            # check if the (POS tag, word) tuple exists as a key in emission counts
            if key in emis_keys: # complete this line

                # Get the count of (POS tag, word) from the emission_counts d
                count = emission_counts[key]

            # Get the count of the POS tag
            count_tag = tag_counts[all_tags[i]]

            # Apply smoothing and store the smoothed value
            # into the emission matrix B for this row and column
            B[i,j] = (count+alpha)/(count_tag+num_words*alpha)

    return B

# creating your emission probability matrix. this takes a few minutes to run.
B = create_emission_matrix(alpha, tag_counts, emission_counts, list(vocab))

print(f"View Matrix position at row 0, column 0: {B[0,0]:.9f}")
print(f"View Matrix position at row 3, column 1: {B[3,1]:.9f}")

# Try viewing emissions for a few words in a sample dataframe
cidx  = ['725','adroitly','engineers', 'promoted', 'synergy']

# Get the integer ID for each word
cols = [vocab[a] for a in cidx]

# Choose POS tags to show in a sample dataframe
rvals =['CD','NN','NNS', 'VB','RB','RP']

# For each POS tag, get the row number from the 'states' list
rows = [states.index(a) for a in rvals]

# Get the emissions for the sample of words, and the sample of POS tags
B_sub = pd.DataFrame(B[np.ix_(rows,cols)], index=rvals, columns = cidx )
print(B_sub)

# Part 3: Viterbi Algorithm and Dynamic Programming
# UNQ_C5
def initialize(states, tag_counts, A, B, corpus, vocab):
    '''
    Input:
        states: a list of all possible parts-of-speech
        tag_counts: a dictionary mapping each tag to its respective count
        A: Transition Matrix of dimension (num_tags, num_tags)
        B: Emission Matrix of dimension (num_tags, len(vocab))
        corpus: a sequence of words whose POS is to be identified in a list
        vocab: a dictionary where keys are words in vocabulary and value is an index
    Output:
        best_probs: matrix of dimension (num_tags, len(corpus)) of floats
        best_paths: matrix of dimension (num_tags, len(corpus)) of integers
    '''
    # Get the total number of unique POS tags
    num_tags = len(tag_counts)

    # Initialize best_probs matrix
    # POS tags in the rows, number of words in the corpus as the columns
    best_probs = np.zeros((num_tags, len(corpus)))

    # Initialize best_paths matrix
    # POS tags in the rows, number of words in the corpus as columns
    best_paths = np.zeros((num_tags, len(corpus)), dtype=int)

    # Define the start token
    s_idx = states.index("--s--")

    # Go through each of the POS tags
    for i in range(num_tags): # complete this line

        # Handle the special case when the transition from start token to POS tag i is zero
        if A[s_idx,i]==0: # complete this line

            # Initialize best_probs at POS tag 'i', column 0, to negative infinity
            best_probs[i,0] = float('-inf')

        # For all other cases when transition from start token to POS tag i is non-zero:
        else:

            # Initialize best_probs at POS tag 'i', column 0
            # Check the formula in the instructions above
            best_probs[i,0] = math.log(A[s_idx,i]) + math.log(B[i,vocab[corpus[0]]])

    return best_probs, best_paths

best_probs, best_paths = initialize(states, tag_counts, A, B, prep, vocab)
# Test the function
print(f"best_probs[0,0]: {best_probs[0,0]:.4f}")
print(f"best_paths[2,3]: {best_paths[2,3]:.4f}")

# UNQ_C6
def viterbi_forward(A, B, test_corpus, best_probs, best_paths, vocab):
    '''
    Input:
        A, B: The transition and emission matrices respectively
        test_corpus: a list containing a preprocessed corpus
        best_probs: an initilized matrix of dimension (num_tags, len(corpus))
        best_paths: an initilized matrix of dimension (num_tags, len(corpus))
        vocab: a dictionary where keys are words in vocabulary and value is an index
    Output:
        best_probs: a completed matrix of dimension (num_tags, len(corpus))
        best_paths: a completed matrix of dimension (num_tags, len(corpus))
    '''
    # Get the number of unique POS tags (which is the num of rows in best_probs)
    num_tags = best_probs.shape[0]

    # Go through every word in the corpus starting from word 1
    # Recall that word 0 was initialized in `initialize()`
    for i in range(1, len(test_corpus)):

        # Print number of words processed, every 5000 words
        if i % 5000 == 0:
            print("Words processed: {:>8}".format(i))

        # For each unique POS tag that the current word can be
        for j in range(num_tags): # complete this line

            # Initialize best_prob for word i to negative infinity
            best_prob_i = float('-inf')

            # Initialize best_path for current word i to None
            best_path_i = None

            # For each POS tag that the previous word can be:
            for k in range(num_tags): # complete this line

                # Calculate the probability =
                # best probs of POS tag k, previous word i-1 +
                # log(prob of transition from POS k to POS j) +
                # log(prob that emission of POS j is word i)
                prob = best_probs[k,i-1] + math.log(A[k,j]) + math.log(B[j,vocab[test_corpus[i]]])

                # check if this path's probability is greater than
                # the best probability up to and before this point
                if prob > best_prob_i: # complete this line

                    # Keep track of the best probability
                    best_prob_i = prob

                    # keep track of the POS tag of the previous word
                    # that is part of the best path.
                    # Save the index (integer) associated with
                    # that previous word's POS tag
                    best_path_i = k

            # Save the best probability for the
            # given current word's POS tag
            # and the position of the current word inside the corpus
            best_probs[j,i] = best_prob_i

            # Save the unique integer ID of the previous POS tag
            # into best_paths matrix, for the POS tag of the current word
            # and the position of the current word inside the corpus.
            best_paths[j,i] = best_path_i

    return best_probs, best_paths

# this will take a few minutes to run => processes ~ 30,000 words
best_probs, best_paths = viterbi_forward(A, B, prep, best_probs, best_paths, vocab)

# Test this function
print(f"best_probs[0,1]: {best_probs[0,1]:.4f}")
print(f"best_probs[0,4]: {best_probs[0,4]:.4f}")

# UNQ_C7
# UNQ_C7 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: viterbi_backward
def viterbi_backward(best_probs, best_paths, corpus, states):
    '''
    This function returns the best path.

    '''
    # Get the number of words in the corpus
    # which is also the number of columns in best_probs, best_paths
    m = best_paths.shape[1]

    # Initialize array z, same length as the corpus
    z = [None] * m

    # Get the number of unique POS tags
    num_tags = best_probs.shape[0]

    # Initialize the best probability for the last word
    best_prob_for_last_word = float('-inf')

    # Initialize pred array, same length as corpus
    pred = [None] * m

    ## Step 1 ##

    # Go through each POS tag for the last word (last column of best_probs)
    # in order to find the row (POS tag integer ID)
    # with highest probability for the last word
    for k in range(num_tags): # complete this line

        # If the probability of POS tag at row k
        # is better than the previously best probability for the last word:
        if best_probs[k,-1] > best_prob_for_last_word: # complete this line

            # Store the new best probability for the last word
            best_prob_for_last_word = best_probs[k,m-1]

            # Store the unique integer ID of the POS tag
            # which is also the row number in best_probs
            z[m - 1] = k

    # Convert the last word's predicted POS tag
    # from its unique integer ID into the string representation
    # using the 'states' list
    # store this in the 'pred' array for the last word
    pred[m - 1] = states[k]

    ## Step 2 ##
    # Find the best POS tags by walking backward through the best_paths
    # From the last word in the corpus to the 0th word in the corpus
    for i in range(m-1, -1, -1): # complete this line

        # Retrieve the unique integer ID of
        # the POS tag for the word at position 'i' in the corpus
        pos_tag_for_word_i = z[i]

        # In best_paths, go to the row representing the POS tag of word i
        # and the column representing the word's position in the corpus
        # to retrieve the predicted POS for the word at position i-1 in the corpus
        z[i - 1] = best_paths[pos_tag_for_word_i,i]

        # Get the previous word's POS tag in string form
        # Use the 'states' list,
        # where the key is the unique integer ID of the POS tag,
        # and the value is the string representation of that POS tag
        pred[i - 1] = states[z[i - 1]]

    return pred

# Run and test your function
pred = viterbi_backward(best_probs, best_paths, prep, states)
m=len(pred)
print('The prediction for pred[-7:m-1] is: \n', prep[-7:m-1], "\n", pred[-7:m-1], "\n")
print('The prediction for pred[0:8] is: \n', pred[0:7], "\n", prep[0:7])

print('The third word is:', prep[3])
print('Your prediction is:', pred[3])
print('Your corresponding label y is: ', y[3])

# UNQ_C8
# UNQ_C8 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: compute_accuracy
def compute_accuracy(pred, y):
    '''
    Input:
        pred: a list of the predicted parts-of-speech
        y: a list of lines where each word is separated by a '\t' (i.e. word \t tag)
    Output:

    '''
    num_correct = 0
    total = 0

    # Zip together the prediction and the labels
    for prediction, y in zip(pred, y):
        # Split the label into the word and the POS tag
        word_tag_tuple = y.split()

        # Check that there is actually a word and a tag
        # no more and no less than 2 items
        if len(word_tag_tuple) != 2: # complete this line
            continue

        #print(word_tag_tuple)
        # store the word and tag separately
        word, tag = word_tag_tuple

        # Check if the POS tag label matches the prediction
        if prediction==tag: # complete this line

            # count the number of times that the prediction
            # and label match
            num_correct += 1

        # keep track of the total number of examples (that have valid labels)
        total += 1

    return num_correct/total

print(f"Accuracy of the Viterbi algorithm is {compute_accuracy(pred, y):.4f}")


# -*- coding: utf-8 -*-
"""
`torch.nn` 이 *실제로* 무엇인가요?
=====================================
저자: Jeremy Howard, `fast.ai <https://www.fast.ai>`_.

도움: Rachel Thomas, Francisco Ingham.

번역: `남상호 <https://github.com/namdori61>`_

"""

###############################################################################
# 이 튜토리얼을 스크립트가 아닌 노트북으로 실행하기를 권장합니다. 노트북 (.ipynb) 파일을 다운 받으시려면,
# 페이지 상단에 있는 링크를 클릭해주세요.
#
# PyTorch 는 여러분이 신경망(neural network)를 생성하고 학습시키는 것을 도와주기 위해서
# `torch.nn <https://pytorch.org/docs/stable/nn.html>`_ ,
# `torch.optim <https://pytorch.org/docs/stable/optim.html>`_ ,
# `Dataset <https://pytorch.org/docs/stable/data.html?highlight=dataset#torch.utils.data.Dataset>`_ ,
# 그리고 `DataLoader <https://pytorch.org/docs/stable/data.html?highlight=dataloader#torch.utils.data.DataLoader>`_
# 와 같은 잘 디자인된 모듈과 클래스들을 제공합니다.
# 이들의 성능을 최대한 활용하고 여러분의 문제에 맞게 커스터마이즈하기 위해서,
# 정확히 이들이 어떤 작업을 수행하는지 이해할 필요가 있습니다.
# 이해를 증진하기 위해서, 우리는 먼저 이들 모델들로 부터 아무 피쳐도 사용하지 않고
# MNIST 데이터셋에 대해 기초적인 신경망을 학습시킬 것입니다;
# 우리는 처음에는 가장 기초적인 PyTorch 텐서(tensor) 기능만을 사용할 것입니다.
# 그리고나서 우리는 점차적으로 ``torch.nn``, ``torch.optim``, ``Dataset``, 또는
# ``DataLoader`` 로부터 한번에 하나씩 피쳐를 추가하면서, 정확히 각 부분이 어떤 일을 하는지 그리고
# 이것이 어떻게 코드를 더 정확하고 유연하게 만드는지 보여줄 것입니다.
#
# **이 튜토리얼은 여러분이 이미 PyTorch를 설치하였고, 그리고 텐서 연산의 기초에 대해 익숙하다고 가정합니다.**
# (만약 여러분이 Numpy 배열(array) 연산에 익숙하다면, 여기에서 사용되는 PyTorch 텐서 연산도
# 거의 동일하다는 것을 알게 될 것입니다).
#
# MNIST 데이터 준비
# -------------------
#
# 우리는 손으로 쓴 숫자(0에서 9 사이)의 흑백 이미지로 구성된 클래식
# `MNIST <http://deeplearning.net/data/mnist/>`_ 데이터셋을 사용할 것 입니다.
#
# 우리는 경로 설정을 담당하는 (Python3 표준 라이브러리의 일부인)
# `pathlib <https://docs.python.org/3/library/pathlib.html>`_ 을 사용할 것이고,
# `requests <http://docs.python-requests.org/en/master/>`_ 를 이용하여
# 데이터셋을 다운로드 할 것입니다. 우리는 모듈을 사용할 때만 임포트(import) 할 것이므로,
# 여러분은 매 포인트마다 정확히 어떤 것이 사용되는지 확인할 수 있습니다.

from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)

URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

###############################################################################
# 이 데이터셋은 numpy 배열 포맷이고, 데이터를 직렬화하기 위한
# python 전용 포맷 pickle 을 이용하여 저장되어 있습니다.

import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

###############################################################################
# 각 이미지는 28 x 28 형태 이고, 784 (=28x28) 크기를 가진 하나의 행으로 저장되어 있습니다.
# 하나를 살펴 봅시다; 먼저 우리는 이 이미지를 2d로 재구성해야 합니다.

from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

###############################################################################
# PyTorch는 numpy 배열 보다는 ``torch.tensor`` 를 사용하므로, 우리는 데이터를 변환해야 합니다.

import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())

###############################################################################
# torch.nn 없이 밑바닥부터 신경망 만들기
# ---------------------------------------------
#
# PyTorch 텐서 연산만으로 첫 모델을 만들어봅시다.
# 여러분이 신경망의 기초에 대해서 이미 익숙하다고 가정합니다.
# (만약 익숙하지 않다면 `course.fast.ai <https://course.fast.ai>`_ 에서 학습할 수 있습니다).
#
# PyTorch는 랜덤 또는 0으로만 이루어진 텐서를 생성하는 메서드를 제공하고,
# 우리는 간단한 선형 모델의 가중치(weights)와 절편(bias)을 생성하기 위해서 이것을 사용할 것입니다.
# 이들은 일반적인 텐서에 매우 특별한 한 가지가 추가된 것입니다: 우리는 PyTorch에게 이들이
# 기울기(gradient)가 필요하다고 알려줍니다.
# 이를 통해 PyTorch는 텐서에 행해지는 모든 연산을 기록하게 하고,
# 따라서 *자동적으로* 역전파(back-propagation) 동안에 기울기를 계산할 수 있습니다!
#
# 가중치에 대해서는 ``requires_grad`` 를 초기화(initialization) **다음에** 설정합니다,
# 왜냐하면 우리는 해당 단계가 기울기에 포함되는 것을 원치 않기 때문입니다.
# (PyTorch에서 ``_`` 다음에 오는 메서드 이름은 연산이 인플레이스(in-place)로 수행되는 것을 의미합니다.)
#
# .. note:: `Xavier initialisation <http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf>`_
#    기법을 이용하여 가중치를 초기화 합니다. (1/sqrt(n)을 곱해주는 것을 통해서 초기화).

import math

weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)

###############################################################################
# PyTorch의 기울기를 자동으로 계산해주는 기능 덕분에, Python 표준 함수
# (또는 호출 가능한 객체)를 모델로 사용할 수 있습니다!
# 그러므로 간단한 선형 모델을 만들기 위해서 단순한 행렬 곱셈과 브로드캐스트(broadcast)
# 덧셈을 사용하여 보겠습니다. 또한, 우리는 활성화 함수(activation function)가 필요하므로,
# `log_softmax` 를 구현하고 사용할 것입니다.
# PyTorch에서 많은 사전 구현된 손실 함수(loss function), 활성화 함수들이 제공되지만,
# 일반적인 python을 사용하여 자신만의 함수를 쉽게 작성할 수 있음을 기억해주세요.
# PyTorch는 심지어 여러분의 함수를 위해서 빠른 GPU 또는 벡터화된 CPU 코드를 만들어줄 것입니다.

def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)

###############################################################################
# 위에서, ``@`` 기호는 점곱(dot product) 연산을 나타냅니다.
# 우리는 하나의 배치(batch) 데이터(이 경우에는 64개의 이미지들)에 대하여 함수를 호출할 것입니다.
# 이것은 하나의 *포워드 전달(forward pass)* 입니다. 이 단계에서 우리는 무작위(random) 가중치로
# 시작했기 때문에 우리의 예측이 무작위 예측보다 전혀 나은 점이 없을 것입니다.

bs = 64  # 배치 사이즈

xb = x_train[0:bs]  # x로부터 미니배치(mini-batch) 추출
preds = model(xb)  # 예측
preds[0], preds.shape
print(preds[0], preds.shape)

###############################################################################
# 여러분이 보시듯이, ``preds`` 텐서(tensor)는 텐서 값 외에도, 또한
# 기울기 함수(gradient function)를 담고 있습니다.
# 우리는 나중에 이것을 역전파(backpropagation)를 위해 사용할 것입니다.
# 이제 손실함수(loss function)로 사용하기 위한 음의 로그 우도(negative log-likelihood)를
# 구현합시다. (다시 말하지만, 우리는 표준 Python을 사용할 수 있습니다.):

def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll

###############################################################################
# 우리의 무작위 모델에 대한 손실을 점검해봅시다, 그럼으로써 우리는 나중에 역전파 이후에 개선이 있는지
# 확인할 수 있습니다.

yb = y_train[0:bs]
print(loss_func(preds, yb))

###############################################################################
# 또한, 우리 모델의 정확도(accuracy)를 계산하기 위한 함수를 구현합시다.
# 매 예측마다, 만약 가장 큰 값의 인덱스가 목표값(target value)과 동일하다면,
# 그 예측은 올바른 것입니다.

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

###############################################################################
# 우리의 무작위 모델의 정확도를 점검해 봅시다, 그럼으로써 손실이 개선됨에 따라서 정확도가 개선되는지
# 확인할 수 있습니다.

print(accuracy(preds, yb))

###############################################################################
# 이제 우리는 훈련 루프(training loop)를 실행할 수 있습니다. 매 반복마다, 우리는 다음을 수행할 것입니다:
#
# - 데이터의 미니배치를 선택 (``bs`` 사이즈)
# - 모델을 이용하여 예측 수행
# - 손실 계산
# - ``loss.backward()`` 를 이용하여 모델의 기울기 업데이트, 이 경우에는, ``weights`` 와 ``bias``.
#
# 이제 우리는 이 기울기들을 이용하여 가중치와 절편을 업데이트 합니다.
# 우리는 이것을 ``torch.no_grad()`` 컨텍스트 매니져(context manager) 내에서 실행합니다,
# 왜냐하면 이러한 실행이 다음 기울기의 계산에 기록되지 않기를 원하기 때문입니다.
# PyTorch의 자동 기울기(Autograd)가 어떻게 연산을 기록하는지
# `여기 <https://pytorch.org/docs/stable/notes/autograd.html>`_ 에서 더 알아볼 수 있습니다.
#
# 우리는 그러고나서 기울기를 0으로 설정합니다, 그럼으로써 다음 루프(loop)에 준비하게 됩니다.
# 그렇지 않으면, 우리의 기울기들은 일어난 모든 연산의 누적 집계를 기록하게 되버립니다.
# (즉, ``loss.backward()`` 가 이미 저장된 것을 대체하기보단, 기존 값에 기울기를 *더하게* 됩니다).
#
# .. tip:: 여러분들은 PyTorch 코드에 대하여 표준 python 디버거(debugger)를 사용할 수 있으므로,
#    매 단계마다 다양한 변수 값을 점검할 수 있습니다.
#    아래에서 ``set_trace()`` 를 주석 해제하여 사용해보세요.
#

from IPython.core.debugger import set_trace

lr = 0.5  # 학습률(learning rate)
epochs = 2  # 훈련에 사용할 에포크(epoch) 수

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        #         set_trace()
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()

###############################################################################
# 이제 다 됐습니다: 우리는 제일 간단한 신경망(neural network)의 모든 것을 밑바닥부터 생성하고
# 훈련하였습니다! (이번에는 은닉층(hidden layer)이 없기 때문에,
# 로지스틱 회귀(logistic regression)입니다).
#
# 이제 손실과 정확도를 이전 값들과 비교하면서 확인해봅시다.
# 우리는 손실은 감소하고, 정확도는 증가하기를 기대할 것이고, 그들은 아래와 같습니다.

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

###############################################################################
# torch.nn.functional 사용하기
# ------------------------------
#
# 이제 우리는 코드를 리팩토링(refactoring) 하겠습니다, 그럼으로써 이전과 동일하지만,
# PyTorch의 ``nn`` 클래스의 장점을 활용하여 더 간결하고 유연하게 만들 것입니다.
# 지금부터 매 단계에서, 우리는 코드를 더 짧고, 이해하기 쉽고, 유연하게 만들어야 합니다.
#
# 처음이면서 우리의 코드를 짧게 만들기 가장 쉬운 단계는 직접 작성한 활성화, 손실 함수를
# ``torch.nn.functional`` 의 함수로 대체하는 것입니다
# (관례에 따라, 일반적으로 ``F`` 네임스페이스(namespace)를 통해 임포트(import) 합니다).
# 이 모듈에는 ``torch.nn`` 라이브러리의 모든 함수가 포함되어 있습니다
# (라이브러리의 다른 부분에는 클래스가 포함되어 있습니다.)
# 다양한 손실 및 활성화 함수 뿐만 아니라, 풀링(pooling) 함수와 같이 신경망을 만드는데
# 편리한 몇 가지 함수도 여기에서 찾을 수 있습니다.
# (컨볼루션(convolution) 연산, 선형(linear) 레이어, 등을 수행하는 함수도 있지만,
# 앞으로 보시겠지만 일반적으로 라이브러리의 다른 부분을 사용하여 더 잘 처리 할 수 있습니다.)
#
# 만약 여러분들이 음의 로그 우도 손실과 로그 소프트맥스 (log softmax) 활성화 함수를 사용하는 경우,
# Pytorch는 이 둘을 결합하는 단일 함수인 ``F.cross_entropy`` 를 제공합니다.
# 따라서 모델에서 활성화 함수를 제거할 수도 있습니다.

import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias

###############################################################################
# 더이상 ``model`` 함수에서 ``log_softmax`` 를 호출하지 않고 있습니다.
# 손실과 정확도과 이전과 동일한지 확인해봅시다:

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

###############################################################################
# nn.Module 을 이용하여 리팩토링 하기
# --------------------------------------
# 다음으로, 더 명확하고 간결한 훈련 루프를 위해 ``nn.Module`` 및 ``nn.Parameter`` 를 사용합니다.
# 우리는 ``nn.Module`` (자체가 클래스이고 상태를 추척할 수 있는) 하위 클래스(subclass)를 만듭니다.
# 이 경우에는, 포워드(forward) 단계에 대한 가중치, 절편, 그리고 메소드(method) 등을 유지하는
# 클래스를 만들고자 합니다.
# ``nn.Module`` 은 우리가 사용할 몇 가지 속성(attribute)과 메소드를 (``.parameters()`` 와
# ``.zero_grad()`` 같은) 가지고 있습니다.
#
# .. note:: ``nn.Module`` (대문자 M) 은 PyTorch 의 특정 개념이고, 우리는 이 클래스를
#    많이 사용할 것입니다. ``nn.Module`` 를 Python 의 코드를 임포트하기 위한 코드 파일인
#    `module <https://docs.python.org/3/tutorial/modules.html>`_ (소문자 ``m``)
#    의 개념과 헷갈리지 말아주세요.

from torch import nn

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
        self.bias = nn.Parameter(torch.zeros(10))

    def forward(self, xb):
        return xb @ self.weights + self.bias

###############################################################################
# 함수를 사용하는 대신에 이제는 오브젝트(object) 를 사용하기 때문에,
# 먼저 모델을 인스턴스화(instantiate) 해야 합니다:

model = Mnist_Logistic()

###############################################################################
# 이제 우리는 이전과 동일한 방식으로 손실을 계산할 수 있습니다.
# 여기서 ``nn.Module`` 오브젝트들은 마치 함수처럼 사용됩니다 (즉, 이들은 *호출가능* 합니다),
# 그러나 배후에서 Pytorch 는 우리의 ``forward`` 메소드를 자동으로 호출합니다.

print(loss_func(model(xb), yb))

###############################################################################
# 이전에는 훈련 루프를 위해 이름 별로 각 매개변수(parameter)의 값을 업데이트하고 다음과 같이
# 각 매개 변수에 대한 기울기들을 개별적으로 수동으로 0으로 제거해야 했습니다:
#
# ::
#
#   with torch.no_grad():
#       weights -= weights.grad * lr
#       bias -= bias.grad * lr
#       weights.grad.zero_()
#       bias.grad.zero_()
#
#
# 이제 우리는 model.parameters() 및 model.zero_grad() (모두
# ``nn.Module`` 에 대해 PyTorch에 의해 정의됨)를 활용하여 이러한 단계를 더 간결하게
# 만들고, 특히 더 복잡한 모델에 대해서 일부 매개변수를 잊어 버리는 오류를 덜 발생시킬 수 있습니다:
#
# ::
#
#   with torch.no_grad():
#       for p in model.parameters(): p -= p.grad * lr
#       model.zero_grad()
#
#
# 이제 이것을 나중에 다시 실행할 수 있도록 ``fit`` 함수로 작은 훈련 루프를 감쌀 것입니다.

def fit():
    for epoch in range(epochs):
        for i in range((n - 1) // bs + 1):
            start_i = i * bs
            end_i = start_i + bs
            xb = x_train[start_i:end_i]
            yb = y_train[start_i:end_i]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()

###############################################################################
# 손실이 줄어들었는지 다시 한번 확인합시다:

print(loss_func(model(xb), yb))

###############################################################################
# nn.Linear 를 이용하여 리팩토링 하기
# ------------------------------------
#
# 계속해서 코드를 리팩토링 합니다. ``self.weights`` 및 ``self.bias`` 를 수동으로 정의 및
# 초기화하고, ``xb  @ self.weights + self.bias`` 를 계산하는 대신에,
# 위의 모든 것을 해줄 Pytorch 클래스인
# `nn.Linear <https://pytorch.org/docs/stable/nn.html#linear-layers>`_ 를 선형
# 레이어로 사용합니다.
# Pytorch 에는 다양한 유형의 코드를 크게 단순화 할 수 있는 미리 정의된 레이어가 있고 이는 또한
# 종종 기존 코드보다 속도를 빠르게 합니다.

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)

    def forward(self, xb):
        return self.lin(xb)

###############################################################################
# 이전과 같은 방식으로 모델을 인스턴스화하고 손실을 계산합니다:

model = Mnist_Logistic()
print(loss_func(model(xb), yb))

###############################################################################
# 우리는 여전히 이전과 동일한 ``fit`` 메소드를 사용할 수 있습니다.

fit()

print(loss_func(model(xb), yb))

###############################################################################
# optim 을 이용하여 리팩토링 하기
# ---------------------------------
#
# Pytorch에는 다양한 최적화(optimization) 알고리즘을 가진 패키지인 ``torch.optim`` 도 있습니다.
# 각 매개변수를 수동으로 업데이트 하는 대신, 옵티마이저(optimizer)의 ``step`` 메소드를 사용하여
# 업데이트를 진행할 수 있습니다.
#
# 이렇게 하면 이전에 수동으로 코딩한 최적화 단계를 대체할 수 있습니다:
#
# ::
#
#   with torch.no_grad():
#       for p in model.parameters(): p -= p.grad * lr
#       model.zero_grad()
#
# 대신에 이렇게 말이죠:
#
# ::
#
#   opt.step()
#   opt.zero_grad()
#
# (``optim.zero_grad()`` 는 기울기를 0으로 재설정 해줍니다. 다음 미니 배치에 대한
# 기울기를 계산하기 전에 호출해야 합니다.)

from torch import optim

###############################################################################
# 나중에 다시 사용할 수 있도록 모델과 옵티마이져를 만드는 작은 함수를 정의합니다.

def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)

model, opt = get_model()
print(loss_func(model(xb), yb))

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

###############################################################################
# Dataset 을 이용하여 리팩토링하기
# ----------------------------------
#
# PyTorch 에는 추상 Dataset 클래스가 있습니다. Dataset 은
# ``__len__`` 함수 (Python의 표준 ``len`` 함수에 의해 호출됨) 및
# ``__getitem__`` 함수를 가진 어떤 것이라도 될 수 있으며, 이 함수들을 인덱싱(indexing)하기
# 위한 방법으로 사용합니다.
# `이 튜토리얼 <https://tutorials.pytorch.kr/beginner/data_loading_tutorial.html>`_
# 은 ``Dataset`` 의 하위 클래스로써, 사용자 지정 ``FacialLandmarkDataset`` 클래스를 만드는
# 좋은 예를 제시합니다.
#
# PyTorch 의 `TensorDataset <https://pytorch.org/docs/stable/_modules/torch/utils/data/dataset.html#TensorDataset>`_
# 은 텐서를 감싸는(wrapping) Dataset 입니다.
# 길이와 인덱싱 방식을 정의함으로써 텐서의 첫 번째 차원을 따라 반복, 인덱싱 및 슬라이스(slice)하는 방법도 제공합니다.
# 이렇게하면 훈련 할 때 동일한 라인에서 독립(independent) 변수와 종속(dependent) 변수에 쉽게 액세스 할 수 있습니다.

from torch.utils.data import TensorDataset

###############################################################################
# ``x_train`` 및 ``y_train`` 모두 하나의 ``TensorDataset`` 에 합쳐질 수 있습니다,
# 따라서 반복시키고 슬라이스 하기 편리합니다.

train_ds = TensorDataset(x_train, y_train)

###############################################################################
# 이전에는 x 및 y 값의 미니 배치를 별도로 반복해야했습니다:
#
# ::
#
#     xb = x_train[start_i:end_i]
#     yb = y_train[start_i:end_i]
#
#
# 이제 이 두 단계를 함께 수행 할 수 있습니다:
#
# ::
#
#     xb,yb = train_ds[i*bs : i*bs+bs]
#

model, opt = get_model()

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        xb, yb = train_ds[i * bs: i * bs + bs]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

###############################################################################
# DataLoader 를 이용하여 리팩토링하기
# -----------------------------------
#
# Pytorch 의 ``DataLoader`` 는 배치 관리를 담당합니다.
# 여러분들은 모든 ``Dataset`` 으로부터 ``DataLoader`` 를 생성할 수 있습니다.
# ``DataLoader`` 는 배치들에 대해서 반복하기 쉽게 만들어줍니다.
# ``train_ds[i*bs : i*bs+bs]`` 를 사용하는 대신,
# DataLoader 는 매 미니배치를 자동적으로 제공합니다.

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

###############################################################################
# 이전에는 루프가 다음과 같이 배치 (xb, yb)를 반복했습니다:
#
# ::
#
#       for i in range((n-1)//bs + 1):
#           xb,yb = train_ds[i*bs : i*bs+bs]
#           pred = model(xb)
#
# 이제 (xb, yb)가 DataLoader 에서 자동으로 로드되므로 루프가 훨씬 깨끗해졌습니다:
#
# ::
#
#       for xb,yb in train_dl:
#           pred = model(xb)

model, opt = get_model()

for epoch in range(epochs):
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

###############################################################################
# Pytorch의 nn.Module, nn.Parameter, Dataset 및 DataLoader 덕분에 이제 훈련 루프가
# 훨씬 더 작아지고 이해하기 쉬워졌습니다.
# 이제 실제로 효과적인 모델을 만드는 데 필요한 기본 기능을 추가해 보겠습니다.
#
# 검증(validation) 추가하기
# ---------------------------
#
# 섹션 1에서, 우리는 훈련 데이터에 사용하기 위해 합리적인 훈련 루프를 설정하려고했습니다.
# 실전에서, 여러분들은 과적합(overfitting)을 확인하기 위해서 **항상**
# `검증 데이터셋(validation set) <https://www.fast.ai/2017/11/13/validation-sets/>`_ 이
# 있어야 합니다.
#
# 훈련 데이터를 섞는(shuffling) 것은 배치와 과적합 사이의 상관관계를 방지하기 위해
# `중요합니다. <https://www.quora.com/Does-the-order-of-training-data-matter-when-training-neural-networks>`_
# 반면에, 검증 손실(validation loss)은 검증 데이터셋을 섞든 안섞든 동일합니다.
# 데이터를 섞는 것은 추가 시간이 걸리므로, 검증 데이터를 섞는 것은 의미가 없습니다.
#
# 검증 데이터셋에 대한 배치 사이즈는 학습 데이터셋 배치 크기의 2배를 사용할 것입니다.
# 이는 검증 데이터셋에 대해서는 역전파(backpropagation)가 필요하지 않으므로 메모리를
# 덜 사용하기 때문입니다 (기울기를 저장할 필요가 없음).
# 더 큰 배치 크기를 사용하여 손실을 더 빨리 계산하기 위해 이렇게 합니다.

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)

###############################################################################
# 각 에포크가 끝날 때 검증 손실을 계산하고 프린트 할 것입니다.
#
# (훈련 전에 항상 ``model.train()`` 을 호출하고, 추론(inference) 전에 ``model.eval()``
# 을 호출합니다, 이는 ``nn.BatchNorm2d`` 및 ``nn.Dropout`` 과 같은 레이어에서
# 이러한 다른 단계(훈련, 추론) 에 대한 적절한 동작이 일어나게 하기 위함입니다.)

model, opt = get_model()

for epoch in range(epochs):
    model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

    model.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)

    print(epoch, valid_loss / len(valid_dl))

###############################################################################
# fit() 와 get_data() 생성하기
# ----------------------------------
#
# 이제 우리는 우리만의 작은 리팩토링을 수행할 것입니다.
# 훈련 데이터셋과 검증 데이터셋 모두에 대한 손실을 계산하는 유사한 프로세스를 두 번 거치므로,
# 이를 하나의 배치에 대한 손실을 계산하는 자체 함수 ``loss_batch`` 로 만들어보겠습니다.
#
# 훈련 데이터셋에 대한 옵티마이저를 전달하고 이를 사용하여 역전파를 수행합니다.
# 검증 데이터셋의 경우 옵티마이저를 전덜하지 않으므로 메소드가 역전파를 수행하지 않습니다.

def loss_batch(model, loss_func, xb, yb, opt=None):
    loss = loss_func(model(xb), yb)

    if opt is not None:
        loss.backward()
        opt.step()
        opt.zero_grad()

    return loss.item(), len(xb)

###############################################################################
# ``fit`` 은 모델을 훈련하고 각 에포크에 대한 훈련 및 검증 손실을 계산하는 작업을 수행합니다.

import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)

        model.eval()
        with torch.no_grad():
            losses, nums = zip(
                *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
            )
        val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)

###############################################################################
# ``get_data`` 는 학습 및 검증 데이터셋에 대한 dataloader 를 출력합니다.

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

###############################################################################
# 이제 dataloader를 가져오고 모델을 훈련하는 전체 프로세스를 3 줄의 코드로 실행할 수 있습니다:

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

###############################################################################
# 이러한 기본 3줄의 코드를 사용하여 다양한 모델을 훈련할 수 있습니다.
# 컨볼루션 신경망(CNN)을 훈련하는 데 사용할 수 있는지 살펴 보겠습니다!
#
# CNN 으로 넘어가기
# --------------------
#
# 이제 3개의 컨볼루션 레이어로 신경망을 구축할 것입니다.
# 이전 섹션의 어떤 함수도 모델의 형식에 대해 가정하지 않기 때문에,
# 별도의 수정없이 CNN을 학습하는 데 사용할 수 있습니다.
#
# Pytorch 의 사전정의된
# `Conv2d <https://pytorch.org/docs/stable/nn.html#torch.nn.Conv2d>`_ 클래스를
# 컨볼루션 레이어로 사용합니다. 3개의 컨볼루션 레이어로 CNN을 정의합니다.
# 각 컨볼루션 뒤에는 ReLU가 있습니다. 마지막으로 평균 풀링(average pooling)을 수행합니다.
# (``view`` 는 PyTorch의 numpy ``reshape`` 버전입니다.)

class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))

lr = 0.1

###############################################################################
# `모멘텀(Momentum) <https://cs231n.github.io/neural-networks-3/#sgd>`_ 은
# 이전 업데이트도 고려하고 일반적으로 더 빠른 훈련으로 이어지는 확률적 경사하강법(stochastic gradient descent)
# 의 변형입니다.

model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

###############################################################################
# nn.Sequential
# ------------------------
#
# ``torch.nn`` 에는 코드를 간단히 사용할 수 있는 또 다른 편리한 클래스인
# `Sequential <https://pytorch.org/docs/stable/nn.html#torch.nn.Sequential>`_
# 이 있습니다..
# ``Sequential`` 객체는 그 안에 포함된 각 모듈을 순차적으로 실행합니다.
# 이것은 우리의 신경망을 작성하는 더 간단한 방법입니다.
#
# 이를 활용하려면 주어진 함수에서 **사용자정의 레이어(custom layer)** 를 쉽게
# 정의할 수 있어야 합니다.
# 예를 들어, PyTorch에는 `view` 레이어가 없으므로 우리의 신경망 용으로 만들어야 합니다.
# ``Lambda`` 는 ``Sequential`` 로 신경망을 정의할 때 사용할 수 있는 레이어를 생성할 것입니다.

class Lambda(nn.Module):
    def __init__(self, func):
        super().__init__()
        self.func = func

    def forward(self, x):
        return self.func(x)

def preprocess(x):
    return x.view(-1, 1, 28, 28)

###############################################################################
# ``Sequential`` 로 생성된 모들은 간단하게 아래와 같습니다:

model = nn.Sequential(
    Lambda(preprocess),
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AvgPool2d(4),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

###############################################################################
# DataLoader 감싸기
# -----------------------------
#
# 우리의 CNN은 상당히 간결하지만, MNIST에서만 작동합니다, 왜냐하면:
#  - 입력이 28\*28의 긴 벡터라고 가정합니다.
#  - 최종적으로 CNN 그리드 크기는 4\*4 라고 가정합니다. (이것은 우리가 사용한 평균 풀링 커널 크기 때문입니다.)
#
# 이 두 가지 가정을 제거하여 모델이 모든 2d 단일 채널(channel) 이미지에서 작동하도록 하겠습니다.
# 먼저 초기 Lambda 레이어를 제거하고 데이터 전처리를 제네레이터(generator)로 이동시킬 수 있습니다:

def preprocess(x, y):
    return x.view(-1, 1, 28, 28), y

class WrappedDataLoader:
    def __init__(self, dl, func):
        self.dl = dl
        self.func = func

    def __len__(self):
        return len(self.dl)

    def __iter__(self):
        batches = iter(self.dl)
        for b in batches:
            yield (self.func(*b))

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

###############################################################################
# 다음으로 ``nn.AvgPool2d`` 를 ``nn.AdaptiveAvgPool2d`` 로 대체하여 우리가 가진
# *입력* 텐서가 아니라 원하는 *출력* 텐서의 크기를 정의할 수 있습니다.
# 결과적으로 우리 모델은 모든 크기의 입력과 함께 작동합니다.

model = nn.Sequential(
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d(1),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

###############################################################################
# 한번 실행해 봅시다:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

###############################################################################
# GPU 사용하기
# ---------------
#
# 만약 여러분들이 운이 좋아서 CUDA 지원 GPU (대부분의 클라우드 제공 업체에서
# 시간당 약 $0.50 에 이용할 수 있습니다) 를 사용할 수 있다면, 코드 실행 속도를 높일 수 있습니다.
# 먼저 GPU가 Pytorch에서 작동하는지 확인합니다:

print(torch.cuda.is_available())

###############################################################################
# 그리고 이에 대한 디바이스 오브젝트를 생성합니다:

dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")

###############################################################################
# GPU로 배치를 옮기도록 ``preprocess`` 를 업데이트 합시다:

def preprocess(x, y):
    return x.view(-1, 1, 28, 28).to(dev), y.to(dev)

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

###############################################################################
# 마지막으로 모델을 GPU로 이동시킬 수 있습니다.

model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

###############################################################################
# 이제 더 빨리 실행됩니다:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

###############################################################################
# 마치면서
# -----------------
#
# 이제 Pytorch를 사용하여 다양한 유형의 모델을 학습하는 데 사용할 수 있는 일반 데이터 파이프 라인과
# 훈련 루프가 있습니다.
# 이제 모델 학습이 얼마나 간단한지 확인하려면 `mnist_sample` 샘플 노트북을 살펴보세요.
#
# 물론 데이터 증강(data augmentation), 초매개변수 조정(hyperparameter tuning),
# 훈련과정 모니터링(monitoring training), 전이 학습(transfer learning) 등과 같이
# 추가하고 싶은 항목들이 많이 있을 것입니다.
# 이러한 기능들은 이 튜토리얼에 표시된 것과 동일한 설계 접근 방식을 사용하여 개발된 fastai 라이브러리에서
# 사용할 수 있으며, 모델을 더욱 발전시키려는 실무자에게 자연스러운 다음 단계를 제공합니다.
#
# 이 튜토리얼의 시작 부분에서 ``torch.nn``, ``torch.optim``, ``Dataset``,
# 그리고 ``DataLoader`` 의 각 예제를 통해 설명하겠다고 이야기했었습니다.
# 이제 위의 내용들을 요약해보겠습니다:
#
#  - **torch.nn**
#
#    + ``Module``: 함수처럼 동작하지만, 또한 상태(state) (예를 들어, 신경망의 레이어 가중치)를
#      포함할 수 있는 호출 가능한 오브젝트를 생성합니다.
#      이는 포함된 ``Parameter`` (들)가 어떤 것인지 알고, 모든 기울기를 0으로 설정하고 가중치
#      업데이트 등을 위해 반복할 수 있습니다.
#    + ``Parameter``: ``Module`` 에 역전파 동안 업데이트가 필요한 가중치가 있음을 알려주는
#      텐서용 래퍼입니다. `requires_grad` 속성이 설정된 텐서만 업데이트 됩니다.
#    + ``functional``: 활성화 함수, 손실 함수 등을 포함하는 모듈 (관례에 따라 일반적으로
#      ``F`` 네임스페이스로 임포트 됩니다) 이고, 물론 컨볼루션 및 선형 레이어 등에 대해서
#      상태를 저장하지않는(non-stateful) 버전의 레이어를 포함합니다.
#  - ``torch.optim``: 역전파 단계에서 ``Parameter`` 의 가중치를 업데이트하는,
#    ``SGD`` 와 같은 옵티마이저를 포함합니다.
#  - ``Dataset``: ``TensorDataset`` 과 같이 Pytorch와 함께 제공되는 클래스를 포함하여 ``__len__`` 및
#    ``__getitem__`` 이 있는 객체의 추상 인터페이스
#  - ``DataLoader``: 모든 종류의 ``Dataset`` 을 기반으로 데이터의 배치들을 출력하는 반복자(iterator)를 생성합니다.


# coding: utf-8

# In[ ]:

get_ipython().magic(u'load_ext watermark')
get_ipython().magic(u"watermark -d -u -a 'Andreas Mueller, Kyle Kastner, Sebastian Raschka' -v -p numpy,scipy,matplotlib")

# The use of watermark (above) is optional, and we use it to keep track of the changes while developing the tutorial material. (You can install this IPython extension via "pip install watermark". For more information, please see: https://github.com/rasbt/watermark).

# # SciPy 2016 Scikit-learn Tutorial

# # Representation and Visualization of Data

# Machine learning is about fitting models to data; for that reason, we'll start by
# discussing how data can be represented in order to be understood by the computer.  Along
# with this, we'll build on our matplotlib examples from the previous section and show some
# examples of how to visualize data.

# ## Data in scikit-learn

# Data in scikit-learn, with very few exceptions, is assumed to be stored as a
# **two-dimensional array**, of shape `[n_samples, n_features]`. Many algorithms also accept ``scipy.sparse`` matrices of the same shape.

# - **n_samples:**   The number of samples: each sample is an item to process (e.g. classify).
#   A sample can be a document, a picture, a sound, a video, an astronomical object,
#   a row in database or CSV file,
#   or whatever you can describe with a fixed set of quantitative traits.
# - **n_features:**  The number of features or distinct traits that can be used to describe each
#   item in a quantitative manner.  Features are generally real-valued, but may be Boolean or
#   discrete-valued in some cases.
#
# The number of features must be fixed in advance. However it can be very high dimensional
# (e.g. millions of features) with most of them being "zeros" for a given sample. This is a case
# where `scipy.sparse` matrices can be useful, in that they are
# much more memory-efficient than NumPy arrays.
#
# As we recall from the previous section (or Jupyter notebook), we represent samples (data points or instances) as rows in the data array, and we store the corresponding features, the "dimensions," as columns.

# ### A Simple Example: the Iris Dataset

# As an example of a simple dataset, we're going to take a look at the iris data stored by scikit-learn.
# The data consists of measurements of three different iris flower species.  There are three different species of iris
# in this particular dataset as illustrated below:

# Iris Setosa
#
#
# Iris Versicolor
#
#
# Iris Virginica
#
#
#

# ### Quick Question:

# **Let's assume that we are interested in categorizing new observations; we want to predict whether unknown flowers are  Iris-Setosa, Iris-Versicolor, or Iris-Virginica flowers, respectively. Based on what we've discussed in the previous section, how would we construct such a dataset?***
#
# Remember: we need a 2D array of size `[n_samples x n_features]`.
#
# - What would the `n_samples` refer to?
#
# - What might the `n_features` refer to?
#
# Remember that there must be a **fixed** number of features for each sample, and feature
# number *j* must be a similar kind of quantity for each sample.

# ### Loading the Iris Data with Scikit-learn

# For future experiments with machine learning algorithms, we recommend you to bookmark the [UCI machine learning repository](http://archive.ics.uci.edu/ml/), which hosts many of the commonly used datasets that are useful for benchmarking machine learning algorithms -- a very popular resource for machine learning practioners and researchers. Conveniently, some of these datasets are already included in scikit-learn so that we can skip the tedious parts of downloading, reading, parsing, and cleaning these text/CSV files. You can find a list of available datasets in scikit-learn at: http://scikit-learn.org/stable/datasets/#toy-datasets.
#
# For example, scikit-learn has a very straightforward set of data on these iris species.  The data consist of
# the following:
#
# - Features in the Iris dataset:
#
#   1. sepal length in cm
#   2. sepal width in cm
#   3. petal length in cm
#   4. petal width in cm
#
# - Target classes to predict:
#
#   1. Iris Setosa
#   2. Iris Versicolour
#   3. Iris Virginica

#
#
# (Image: "Petal-sepal". Licensed under CC BY-SA 3.0 via Wikimedia Commons - https://commons.wikimedia.org/wiki/File:Petal-sepal.jpg#/media/File:Petal-sepal.jpg)

# ``scikit-learn`` embeds a copy of the iris CSV file along with a helper function to load it into numpy arrays:

# In[1]:

from sklearn.datasets import load_iris
iris = load_iris()

# The resulting dataset is a ``Bunch`` object: you can see what's available using
# the method ``keys()``:

# In[2]:

iris.keys()

# The features of each sample flower are stored in the ``data`` attribute of the dataset:

# In[3]:

n_samples, n_features = iris.data.shape
print('Number of samples:', n_samples)
print('Number of features:', n_features)
# the sepal length, sepal width, petal length and petal width of the first sample (first flower)
print(iris.data[0])

# The information about the class of each sample is stored in the ``target`` attribute of the dataset:

# In[ ]:

print(iris.data.shape)
print(iris.target.shape)

# In[ ]:

print(iris.target)

# In[ ]:

import numpy as np

np.bincount(iris.target)

# Using the NumPy's bincount function (above), we can see that the classes are distributed uniformly in this dataset - there are 50 flowers from each species, where
#
# - class 0: Iris-Setosa
# - class 1: Iris-Versicolor
# - class 2: Iris-Virginica

# These class names are stored in the last attribute, namely ``target_names``:

# In[ ]:

print(iris.target_names)

# This data is four dimensional, but we can visualize one or two of the dimensions
# at a time using a simple histogram or scatter-plot.  Again, we'll start by enabling
# matplotlib inline mode:

# In[ ]:

get_ipython().magic(u'matplotlib inline')
import matplotlib.pyplot as plt

# In[ ]:

x_index = 3
colors = ['blue', 'red', 'green']

for label, color in zip(range(len(iris.target_names)), colors):
    plt.hist(iris.data[iris.target==label, x_index],
             label=iris.target_names[label],
             color=color)

plt.xlabel(iris.feature_names[x_index])
plt.legend(loc='upper right')
plt.show()

# In[ ]:

x_index = 3
y_index = 0

colors = ['blue', 'red', 'green']

for label, color in zip(range(len(iris.target_names)), colors):
    plt.scatter(iris.data[iris.target==label, x_index],
                iris.data[iris.target==label, y_index],
                label=iris.target_names[label],
                c=color)

plt.xlabel(iris.feature_names[x_index])
plt.ylabel(iris.feature_names[y_index])
plt.legend(loc='upper left')
plt.show()

# ### Quick Exercise:

# **Change** `x_index` **and** `y_index` **in the above script
# and find a combination of two parameters
# which maximally separate the three classes.**
#
# This exercise is a preview of **dimensionality reduction**, which we'll see later.

# ## Other Available Data

# [Scikit-learn makes available a host of datasets for testing learning algorithms](http://scikit-learn.org/stable/datasets/#dataset-loading-utilities).
# They come in three flavors:
#
# - **Packaged Data:** these small datasets are packaged with the scikit-learn installation,
#   and can be downloaded using the tools in ``sklearn.datasets.load_*``
# - **Downloadable Data:** these larger datasets are available for download, and scikit-learn
#   includes tools which streamline this process.  These tools can be found in
#   ``sklearn.datasets.fetch_*``
# - **Generated Data:** there are several datasets which are generated from models based on a
#   random seed.  These are available in the ``sklearn.datasets.make_*``
#
# You can explore the available dataset loaders, fetchers, and generators using IPython's
# tab-completion functionality.  After importing the ``datasets`` submodule from ``sklearn``,
# type
#
#     datasets.load_<TAB>
#
# or
#
#     datasets.fetch_<TAB>
#
# or
#
#     datasets.make_<TAB>
#
# to see a list of available functions.

# In[ ]:

from sklearn import datasets

# The data downloaded using the ``fetch_`` scripts are stored locally,
# within a subdirectory of your home directory.
# You can use the following to determine where it is:

# In[ ]:

from sklearn.datasets import get_data_home
get_data_home()

# Be warned: many of these datasets are quite large, and can take a long time to download!
# (especially on Conference wifi).
#
# If you start a download within the IPython notebook
# and you want to kill it, you can use ipython's "kernel interrupt" feature, available in the menu or using
# the shortcut ``Ctrl-m i``.
#
# You can press ``Ctrl-m h`` for a list of all ``ipython`` keyboard shortcuts.

# ## Loading Digits Data

# Now we'll take a look at another dataset, one where we have to put a bit
# more thought into how to represent the data.  We can explore the data in
# a similar manner as above:

# In[ ]:

from sklearn.datasets import load_digits
digits = load_digits()

# In[ ]:

digits.keys()

# In[ ]:

n_samples, n_features = digits.data.shape
print((n_samples, n_features))

# In[ ]:

print(digits.data[0])
print(digits.target)

# The target here is just the digit represented by the data.  The data is an array of
# length 64... but what does this data mean?

# There's a clue in the fact that we have two versions of the data array:
# ``data`` and ``images``.  Let's take a look at them:

# In[ ]:

print(digits.data.shape)
print(digits.images.shape)

# We can see that they're related by a simple reshaping:

# In[ ]:

import numpy as np
print(np.all(digits.images.reshape((1797, 64)) == digits.data))

# Let's visualize the data.  It's little bit more involved than the simple scatter-plot
# we used above, but we can do it rather quickly.

# In[ ]:

# set up the figure
fig = plt.figure(figsize=(6, 6))  # figure size in inches
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

# plot the digits: each image is 8x8 pixels
for i in range(64):
    ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
    ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest')

    # label the image with the target value
    ax.text(0, 7, str(digits.target[i]))

# We see now what the features mean.  Each feature is a real-valued quantity representing the
# darkness of a pixel in an 8x8 image of a hand-written digit.
#
# Even though each sample has data that is inherently two-dimensional, the data matrix flattens
# this 2D data into a **single vector**, which can be contained in one **row** of the data matrix.

# ## Generated Data: the S-Curve

# One dataset often used as an example of a simple nonlinear dataset is the S-cure:

# In[ ]:

from sklearn.datasets import make_s_curve
data, colors = make_s_curve(n_samples=1000)
print(data.shape)
print(colors.shape)

# In[ ]:

from mpl_toolkits.mplot3d import Axes3D
ax = plt.axes(projection='3d')
ax.scatter(data[:, 0], data[:, 1], data[:, 2], c=colors)
ax.view_init(10, -60)

# This example is typically used with an unsupervised learning method called Locally
# Linear Embedding.  We'll explore unsupervised learning in detail later in the tutorial.

# ## Exercise: working with the faces dataset

# Here we'll take a moment for you to explore the datasets yourself.
# Later on we'll be using the Olivetti faces dataset.
# Take a moment to fetch the data (about 1.4MB), and visualize the faces.
# You can copy the code used to visualize the digits above, and modify it for this data.

# In[ ]:

from sklearn.datasets import fetch_olivetti_faces

# In[ ]:

# fetch the faces data

# In[ ]:

# Use a script like above to plot the faces image data.
# hint: plt.cm.bone is a good colormap for this data

# ### Solution:

# In[ ]:

# %load solutions/03A_faces_plot.py

Scipy/03 Data Representation for Machine Learning.py