<jupyter_start><jupyter_text>11장 자연어처리 1부 **감사말**: 프랑소와 숄레의 [Deep Learning with Python, Second Edition](https://www.manning.com/books/deep-learning-with-python-second-edition?a_aid=keras&a_bid=76564dff) 10장에 사용된 코드에 대한 설명을 담고 있으며 텐서플로우 2.6 버전에서 작성되었습니다. 소스코드를 공개한 저자에게 감사드립니다.**tensorflow 버전과 GPU 확인**- 구글 코랩 설정: '런타임 -> 런타임 유형 변경' 메뉴에서 GPU 지정 후 아래 명령어 실행 결과 확인    ```    !nvidia-smi    ```- 사용되는 tensorflow 버전 확인    ```python    import tensorflow as tf    tf.__version__    ```- tensorflow가 GPU를 사용하는지 여부 확인    ```python    tf.config.list_physical_devices('GPU')    ```  주요내용 - 자연어처리(Natural Language Processing) 소개    - 단어주머니(bag-of-words) 모델    - 순차(sequence) 모델- 순차 모델 활용    - 양방향 순환신경망(bidirectional LSTM) 적용- 트랜스포머(Transformer) 활용- 시퀀스-투-시퀀스(seq2seq) 모델 활용  11.1 자연어처리 소개 파이썬, 자바, C, C++, C, 자바스크립트 등 컴퓨터 프로그래밍언어와 구분하기 위해 일상에서 사용되는 한국어, 영어 등을 __자연어__(natural language)라 부른다. 자연어의 특성상 정확한 분석을 위한 알고리즘을 구현하는 일은 사실상 매우 어렵다. 딥러닝 기법이 활용되기 이전깢지 적절한 규칙을 구성하여 자연어를 이해하려는 수 많은 시도가 있어왔지만 별로 성공적이지 않았다.1990년대부터 인터넷으로부터 구해진 엄청난 양의 텍스트 데이터에 머신러닝 기법을적용하기 시작했다. 단, 주요 목적이 **언어의 이해**가 아니라 아래 예제들처럼 입력 텍스트를 분석하여**통계적으로 유용한 정보를 예측**하는 방향으로 수정되었다.- 텍스트 분류: "이 문장의 주제는?"- 내용 필터링: "욕설이 포함되었나?"- 감성 분석: "내용이 긍정이야 부정이야?"- 언어 모델링: "이 문장에 이어 어떤 단어가 있어야 하지?"- 번역: "이거를 한국어로 어떻게 말해?"- 요약: "이 기사를 한 줄로 요약하면?"이와 같은 분석을 **자연어처리**(NLP, Natural Language Processing)이라 하며단어(words), 문장(sentences), 문단(paragraphs) 등에서 찾을 수 있는패턴(pattern)을  인식하려 시도한다.  **머신러닝 활용** 자연어처리를 위해 1990년대부터 시작된 머신러닝 활용의 변화과정은 다음과 같다. - 1990 - 2010년대 초반:     결정트리(decision trees), 로지스틱 회귀(logistic regression) 모델이 주로 활용됨.- 2014-2015: LSTM 등 시퀀스 처리 알고리즘 활용 시작- 2015-2017: (양방향) 순환신경망이 기본적으로 활용됨.- 2017-2018: 트랜스포머(Transformer) 모델이 최고의 성능 발휘하며,     많은 난제들을 해결함. 현재 가장 많이 활용되는 모델임.  11.2 텍스트 벡터화 딥러닝 모델은 텍스트 자체를 처리할 수 없다.따라서 택스트를 수치형 텐서(numeric tensors)로 변환하는 **텍스트 벡터화**(text vectorization) 과정이 요구되며보통 다음 세 단계를 따른다.1. **텍스트 표준화**(text standardization): 소문자화, 마침표 제거 등등1. **토큰화**(tokenization): 기본 단위의 **유닛**(units)으로 쪼개기    - 토큰 예제: 문자, 단어, 단언들의 집합 등등1. **어휘 색인화**(vocabulary indexing): 토큰 각각을 하나의 수치형 벡터(numerical vector)로 변환.아래 그림은 텍스트 벡터화의 기본적인 과정을 잘 보여준다. 그림 출처: [Deep Learning with Python(Manning MEAP)](https://www.manning.com/books/deep-learning-with-python-second-edition) **텍스트 표준화** 다음 두 문장을 표준화를 통해 동일한 문장으로 변환해보자.- "sunset came. i was staring at the Mexico sky. Isnt nature splendid??"- "Sunset came; I stared at the M&eacute;xico sky. Isn't nature splendid?" 예를 들어 다음 표준화 기법을 사용할 수 있다.- 모두 소문자화- `.`, `;`, `?`, `'` 등 특수 기호 제거- 특수 알파벳 변환: "&eacute;"를 "e"로, "&aelig;"를 "ae"로 등등- 동사/명사의 기본형 활용: "cats"를 "[cat]"로, "was staring"과 "stared"를 "[stare]"로 등등. 그러면 위 두 문장 모두 아래 문장으로 변환된다. - "sunset came i [stare] at the mexico sky isnt nature splendid" 표준화 과정을 통해 어느 정도의 정보를 상실하게 되지만학습해야할 내용을 줄여 일반화 성능이 보다 좋은 모델을 훈련시키는 장점이 있다.하지만 분석 목적에 따라 표준화 기법은 경우에 따라 달라질 수 있음에 주의해야 한다. 예를 들어 인터뷰 기사의 경우 물음표(`?`)는 제거하면 안된다. **토큰화** 텍스트 표준화 이후 데이터 분석의 기본 단위인 토큰으로 쪼개야 한다.보통 아래 세 가지 방식 중에 하나를 사용한다.- 단어 기준 토큰화(word-level tokenization)    - 공백으로 구분된 단어들로 쪼개기.     - 경우에 따라 동사 어근과 어미를 구분하기도 함: "star+ing", "call+ed" 등등- N-그램 토큰화(N-gram tokenization)    - N-그램 토큰: 연속으로 위치한 N 개(이하)의 단어 묶음    - 예제: "the cat", "he was" 등은 2-그램 토큰이다.- 문자 기준 토큰화(character-level tokenization)    - 하나의 문자가 하나의 토큰임.    - 문장 생성, 음성 인식 등에서 활용됨. 일반적으로 문자 기준 토큰화는 잘 사용되지 않는다. 여기서도 단어 기준 또는 N-그램 토큰화만 이용한다.- 단어 기준 토큰화: 단어들의 순서를 중요시하는 **순차 모델**(sequence models)을 사용할 경우 활용- N-그램 토큰화: 단언들의 순서를 별로 상관하지 않는 **단어주머니(bag-of-words, BOW)**     모델을 사용할 경우 활용    - N-그램: 단어들 사이의 순서에 대한 지역 정보를 어느 정도 유지함.    - 일종의 특성 공학(feature engineering) 기법이며 따라서         얕은 학습 기반의 언어처리(shallow language-processing) 모델에 활용됨.    - 1차원 합성곱 신경망, 순환 신경망, 트랜스포머 등은 이 기법을 사용하지 않아도 됨. **단어주머니(bag-of-words)**는 N-토큰으로 구성된 집합을 의미하며 **N-그램 주머니(bag-of-N-grams)**라고 불리기도 한다.예를 들어 "the cat sat on the mat." 문장에 대한 2-그램 집합과 3-그램 집합은 각각 다음과 같다. - 2-그램 집합```{"the", "the cat", "cat", "cat sat", "sat", "sat on", "on", "on the", "the mat", "mat"}``` - 3-그램 집합```{"the", "the cat", "cat", "cat sat", "the cat sat", "sat", "sat on", "on", "cat sat on", "on the", "sat on the", "the mat", "mat", "on the mat"} ``` **어휘 색인화** 일반적으로 먼저 훈련셋에 포함된 모든 토큰들의 색인(인덱스)을 작성한다.생성된 색인을 각 토큰을 바탕으로 원-핫, 멀티-핫 인코딩 등의 방식을 사용하여수치형 텐서로 변환한다.[4장](https://codingalzi.github.io/dlp/notebooks/dlp04_getting_started_with_neural_networks.html)과 [5장](https://codingalzi.github.io/dlp/notebooks/dlp05_fundamentals_of_ml.html)에서 설명한 대로 보통 사용 빈도수가 높은 2만 또는 3만 개의 단어만을 대상으로 어휘 색인화를 진행한다.당시에 `num_words=10000`을 사용하여 사용 빈도수가 상위 1만 등 안에 드는 단어만을대상으로 훈련셋을 구성하였다.```pythonfrom tensorflow.keras.datasets import imdb(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)```케라스의 imdb 데이터셋은 이미 정수들의 시퀀스로 전처리가 되어 있다. 하지만 여기서는 원본 imdb 데이터셋을 대상으로 전처리를 직접 수행하는 단계부터 살펴볼 것이다.이를 위해 아래 사항을 기억해 두어야 한다.- OOV 인덱스 활용: 어휘 색인에 포함되지 않는 단어는 모두 1로 처리.     일반 문장으로 번역되는 경우 "[UNK]" 으로 처리됨.    - OOV = Out Of Vocabulary    - UNK = Unknown- 마스크(mask) 토큰: 무신되어야 하는 토큰을 나타냄. 모두 0으로 처리.    - 예를 들어, 문장의 길이를 맞추기 위해 사용되는 패딩으로 0으로 채워줄 수 있음.        ```    [[5,  7, 124, 4, 89]       [8, 34,  21, 0,  0]]     ``` **케라스의 `TextVectorization` 층 활용** 지금까지 설명한 텍스트 벡터화를 위해 케라스의 `TextVectorization` 층을 활용할 수 있으며기본 사용법은 다음과 같다.<jupyter_code>from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
    output_mode="int",
    )<jupyter_output><empty_output><jupyter_text>`TextVectorization` 층 구성에 사용되는 주요 기본 설정은 다음과 같다.- 표준화: 소문자화와 마침표 등 제거    - `standardize='lower_and_strip_punctuation'`- 토큰화: 단어 기준 쪼개기    - `ngrams=None`    - `split='whitespace'`- 출력 모드: 출력 텐서의 형식    - `output_mode="int"` 표준화와 토큰화 방식을 임의로 지정해서 활용할 수도 있다.다만, 파이썬의 기본 문자열 자료형인 `str` 대신에 `tf.string` 텐서를 활용해야 함에 주의해야 한다. 표준화와 토큰화의 기본값은 아래 두 함수를 활용하는 것과 동일하다.- `custom_standardization_fn()`- `custom_split_fn()`<jupyter_code>import re
import string
import tensorflow as tf

# 표준화: 소문자화 및 마침표 제거
def custom_standardization_fn(string_tensor):
    lowercase_string = tf.strings.lower(string_tensor)
    return tf.strings.regex_replace(
        lowercase_string, f"[{re.escape(string.punctuation)}]", "")

# 공백 기준으로 쪼개기
def custom_split_fn(string_tensor):
    return tf.strings.split(string_tensor)

# 사용자 정의 표준화 및 쪼개기 활용
text_vectorization = TextVectorization(
    output_mode="int",
    standardize=custom_standardization_fn,
    split=custom_split_fn,
)<jupyter_output><empty_output><jupyter_text>**예제** 아래 데이터셋을 대상으로 텍스트 벡터화를 진행해보자.<jupyter_code>dataset = [
    "I write, erase, rewrite",
    "Erase again, and then",
    "A poppy blooms.",
]
text_vectorization.adapt(dataset)<jupyter_output><empty_output><jupyter_text>생성된 어휘 색인은 다음과 같다.<jupyter_code>vocabulary = text_vectorization.get_vocabulary()
vocabulary<jupyter_output><empty_output><jupyter_text>생성된 어휘 색인을 활용하여 새로운 문장을 벡터화 해보자.<jupyter_code>test_sentence = "I write, rewrite, and still rewrite again"
encoded_sentence = text_vectorization(test_sentence)
print(encoded_sentence)<jupyter_output>tf.Tensor([ 7  3  5  9  1  5 10], shape=(7,), dtype=int64)<jupyter_text>벡터화된 텐서로부터 문장을 복원하면 표준화된 문장이 생성된다.<jupyter_code>inverse_vocab = dict(enumerate(vocabulary))

decoded_sentence = " ".join(inverse_vocab[int(i)] for i in encoded_sentence)
print(decoded_sentence)<jupyter_output>i write rewrite and [UNK] rewrite again<jupyter_text>**`TextVectorization` 층 사용법** `TextVectorization` 층은 GPU 또는 TPU에서 지원되지 않는다.따라서 모델 구성에 직접 사용하는 방식은 모델의 훈련을늦출 수 있기에 권장되지 않는다.여기서는 대신에 데이터셋 전처리를 모델 구성과 독립적으로 처리하는 방식을 이용한다.하지만 훈련이 완성된 모델을 실전에 배치할 경우 `TextVectorization` 층을완성된 모델에 추가해서 사용하는 게 좋다.이에 대한 자세한 설명은 잠시 뒤에 부록에서 설명한다.  11.3 단어 모음 표현법: 집합과 시퀀스 앞서 언급한 대로 자연어처리 모델에 따라 단어 모음을 다루는 방식이 다르다. - 단어주머니(bag-of-words) 모델    - 단어들의 순서를 무시. 단어 모음을 단어들의 집합으로 다룸.    - 2015년 이전까지 주로 사용됨.- 시퀀스(sequence) 모델    - 순환(recurrent) 모델        - 단어들의 순서를 시계열 데이터의 스텝처럼 간주.        - 2015-2016에 주로 사용됨.    - 트랜스포머(Transformer) 아키텍처        - 기본적으로 순서를 무시하지만 단어 위치를 학습할 수 있는 능력을 가짐.        - 2017년 이후 기본적으로 활용됨. 여기서는 IMDB 영화 리뷰 데이터를 이용하여 두 모델 방식의 활용법과 차이점을 소개한다.  11.3.1 IMDB 영화 리뷰 데이터 준비 이전과는 달리 여기서는 IMDB 데이터셋을 직접 다운로드하여 전처리하는 과정을 살펴본다.  준비 과정 1: 데이터셋 다운로드 압축 풀기압축을 풀면 아래 구조의 디렉토리가 생성된다.```aclImdb/...train/......pos/......neg/...test/......pos/......neg/````train`의 `pos`와 `neg` 서브디렉토리에 각각 12,500개의 긍정과 부정 리뷰가포함되어 있다. *주의사항*: 아래 코드는 윈도우의 경우 10 최신 버전 또는 11부터 지원된다.<jupyter_code>!curl -O https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xf aclImdb_v1.tar.gz<jupyter_output>% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 80.2M  100 80.2M    0     0  26.1M      0  0:00:03  0:00:03 --:--:-- 26.1M<jupyter_text>`aclImdb/train/unsup` 서브디렉토리는 필요 없기에 삭제한다.<jupyter_code>if 'google.colab' in str(get_ipython()):
    !rm -r aclImdb/train/unsup
else: 
    import shutil
    unsup_path = './aclImdb/train/unsup'
    shutil.rmtree(unsup_path)<jupyter_output><empty_output><jupyter_text>긍정 리뷰 하나의 내용을 살펴보자.모델 구성 이전에 훈련 데이터셋을 살펴 보고모델에 대한 직관을 갖는 과정이 항상 필요하다.<jupyter_code>if 'google.colab' in str(get_ipython()):
    !cat aclImdb/train/pos/4077_10.txt
else:
    with open('aclImdb/train/pos/4077_10.txt', 'r') as f:
        text = f.read()
        print(text)<jupyter_output>I first saw this back in the early 90s on UK TV, i did like it then but i missed the chance to tape it, many years passed but the film always stuck with me and i lost hope of seeing it TV again, the main thing that stuck with me was the end, the hole castle part really touched me, its easy to watch, has a great story, great music, the list goes on and on, its OK me saying how good it is but everyone will take there own best bits away with them once they have seen it, yes the animation is top notch and beautiful to watch, it does show its age in a very few parts but that has now become part of it beauty, i am so glad it has came out on DVD as it is one of my top 10 films of all time. Buy it or rent it just see it, best viewing is at night alone with drink and food in reach so you don't have to stop the film.<br /><br />Enjoy<jupyter_text>준비 과정 2: 검증셋 준비훈련셋의 20%를 검증셋으로 떼어낸다.이를 위해 `aclImdb/val` 디렉토리를 생성한 후에긍정과 부정 훈련셋 모두 무작위로 섞은 후 그중 20%를 검증셋 디렉토리로 옮긴다.<jupyter_code>import os, pathlib, shutil, random

base_dir = pathlib.Path("aclImdb")
val_dir = base_dir / "val"
train_dir = base_dir / "train"

for category in ("neg", "pos"):
    os.makedirs(val_dir / category)            # val 디렉토리 생성
    files = os.listdir(train_dir / category)
    
    random.Random(1337).shuffle(files)         # 훈련셋 무작위 섞기
    
    num_val_samples = int(0.2 * len(files))    # 20% 지정 후 검증셋으로 옮기기
    val_files = files[-num_val_samples:]
    
    for fname in val_files:
        shutil.move(train_dir / category / fname,
                    val_dir / category / fname)<jupyter_output><empty_output><jupyter_text>준비 과정 3: 텐서 데이터셋 준비`text_dataset_from_directory()` 함수를 이용하여 훈련셋, 검증셋, 테스트셋을 준비한다. 자료형은 모두 `Dataset`이며, 배치 크기는 32를 사용한다.<jupyter_code>from tensorflow import keras

batch_size = 32

train_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/train", batch_size=batch_size
    )

val_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/val", batch_size=batch_size
    )

test_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/test", batch_size=batch_size
    )<jupyter_output>Found 20000 files belonging to 2 classes.
Found 5000 files belonging to 2 classes.
Found 25000 files belonging to 2 classes.<jupyter_text>각 데이터셋은 배치로 구분되며입력은 `tf.string` 텐서이고, 타깃은 `int32` 텐서이다.크기는 모두 32이며 지정된 배치 크기이다.예를 들어, 첫째 배치의 입력과 타깃 데이터의 정보는 다음과 같다.<jupyter_code>for inputs, targets in train_ds:
    print("inputs.shape:", inputs.shape)
    print("inputs.dtype:", inputs.dtype)
    print("targets.shape:", targets.shape)
    print("targets.dtype:", targets.dtype)
    
    # 예제: 첫째 배치의 첫째 리뷰
    print("inputs[0]:", inputs[0])
    print("targets[0]:", targets[0])
    
    break<jupyter_output>inputs.shape: (32,)
inputs.dtype: <dtype: 'string'>
targets.shape: (32,)
targets.dtype: <dtype: 'int32'>
inputs[0]: tf.Tensor(b'The film begins with a bunch of kids in reform school and focuses on a kid named \'Gabe\', who has apparently worked hard to earn his parole. Gabe and his sister move to a new neighborhood to make a fresh start and soon Gabe meets up with the Dead End Kids. The Kids in this film are little punks, but they are much less antisocial than they\'d been in other previous films and down deep, they are well-meaning punks. However, in this neighborhood there are also some criminals who are perpetrating insurance fraud through arson and see Gabe as a convenient scapegoat--after all, he\'d been to reform school and no one would believe he was innocent once he was framed. So, when Gabe is about ready to be sent back to "The Big House", it\'s up to the rest of the gang to save him and expose the real crooks.<br /><br />The "Dead End Kids" appeared in several Warner Brother[...]<jupyter_text>11.3.2 단어주머니 기법 단어주머니에 채울 토큰으로 어떤 N-그램을 사용할지 먼저 지정해야 한다. - 유니그램(unigrams): 하나의 단어가 한의 토큰- N-그램(N-grams): 최대 N 개의 연속 단어로 이루어진 토큰 **방식 1: 유니그램 바이너리 인코딩** 예를 들어 "the cat sat on the mat" 문장을 유니그램으로 처리하면 다음 단어주머니가 생성된다.집합으로 처리되기에 단어들의 순서는 완전히 무시된다.```{"cat", "mat", "on", "sat", "the"}```이제 모든 문장은 어휘색인에 포함된 단어들의 수만큼 긴 1차원 이진 텐서(binary tensor)로처리된다. 즉, 멀티-핫(multi-hot) 인코딩 방식을 사용해서 텐서로 변환된다.[4장](https://codingalzi.github.io/dlp/notebooks/dlp04_getting_started_with_neural_networks.html)과 [5장](https://codingalzi.github.io/dlp/notebooks/dlp05_fundamentals_of_ml.html)에서 문장을 인코딩 방식과 동일하다. `TextVectorization` 클래스의 `output_mode="multi_hot"` 옵션을 이용하면방금 설명한 내용을 그대로 처리해준다.<jupyter_code>from tensorflow.keras.layers import TextVectorization

text_vectorization = TextVectorization(
    max_tokens=20000,
    output_mode="multi_hot",
)

# 어휘색인 생성
text_only_train_ds = train_ds.map(lambda x, y: x)
text_vectorization.adapt(text_only_train_ds)<jupyter_output><empty_output><jupyter_text>생성된 어휘색인을 이용하여 훈련셋, 검증셋, 테스트셋 모두 벡터화한다.<jupyter_code>binary_1gram_train_ds = train_ds.map(lambda x, y: (text_vectorization(x), y))
binary_1gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
binary_1gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))<jupyter_output><empty_output><jupyter_text>변환된 첫째 배치의 입력과 타깃 데이터의 정보는 다음과 같다.`max_tokens=20000`으로 지정하였기에 모든 문장은 길이가 2만인 벡터로 변환되었다.<jupyter_code>for inputs, targets in binary_1gram_train_ds:
    print("inputs.shape:", inputs.shape)
    print("inputs.dtype:", inputs.dtype)
    print("targets.shape:", targets.shape)
    print("targets.dtype:", targets.dtype)
    print("inputs[0]:", inputs[0])
    print("targets[0]:", targets[0])
    break<jupyter_output>inputs.shape: (32, 20000)
inputs.dtype: <dtype: 'float32'>
targets.shape: (32,)
targets.dtype: <dtype: 'int32'>
inputs[0]: tf.Tensor([1. 1. 1. ... 0. 0. 0.], shape=(20000,), dtype=float32)
targets[0]: tf.Tensor(0, shape=(), dtype=int32)<jupyter_text>**밀집 모델 지정** 단어주머니 모델로 여기서는 밀집 모델을 사용한다. `get_model()` 함수가 컴파일 된 단순한 밀집 모델을 반환한다.모델의 출력값은 긍정일 확률이며, 최상위 층의 활성화 함수로 `sigmoid`를 사용한다.<jupyter_code>from tensorflow import keras
from tensorflow.keras import layers

def get_model(max_tokens=20000, hidden_dim=16):
    inputs = keras.Input(shape=(max_tokens,))
    x = layers.Dense(hidden_dim, activation="relu")(inputs)
    x = layers.Dropout(0.5)(x)
    outputs = layers.Dense(1, activation="sigmoid")(x)  # 긍정일 확률 계산
    
    model = keras.Model(inputs, outputs)
    
    model.compile(optimizer="rmsprop",
                  loss="binary_crossentropy",
                  metrics=["accuracy"])
    
    return model
model = get_model()
model.summary()<jupyter_output>Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 20000)]           0         
                                                                 
 dense (Dense)               (None, 16)                320016    
                                                                 
 dropout (Dropout)           (None, 16)                0         
                                                                 
 dense_1 (Dense)             (None, 1)                 17        
                                                                 
=================================================================
Total params: 320,033
Trainable params: 320,033
Non-trainable params: 0
_________________________________________________________________<jupyter_text>**모델 훈련** 밀집 모델 훈련과정은 특별한 게 없다.훈련 후 테스트셋에 대한 정확도가 89% 보다 조금 낮게 나온다.최고 성능의 모델이 테스트셋에 대해 95% 정도 정확도를 내는 것보다는 낮지만무작위로 찍는 모델보다는 훨씬 좋은 모델이다.<jupyter_code>callbacks = [
    keras.callbacks.ModelCheckpoint("binary_1gram.keras",
                                    save_best_only=True)
]

model.fit(binary_1gram_train_ds.cache(),
          validation_data=binary_1gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)

model = keras.models.load_model("binary_1gram.keras")
print(f"Test acc: {model.evaluate(binary_1gram_test_ds)[1]:.3f}")<jupyter_output>Epoch 1/10
625/625 [==============================] - 10s 16ms/step - loss: 0.4074 - accuracy: 0.8277 - val_loss: 0.2792 - val_accuracy: 0.8908
Epoch 2/10
625/625 [==============================] - 3s 5ms/step - loss: 0.2746 - accuracy: 0.8981 - val_loss: 0.2774 - val_accuracy: 0.8964
Epoch 3/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2471 - accuracy: 0.9115 - val_loss: 0.2872 - val_accuracy: 0.8976
Epoch 4/10
625/625 [==============================] - 3s 5ms/step - loss: 0.2246 - accuracy: 0.9244 - val_loss: 0.3187 - val_accuracy: 0.8936
Epoch 5/10
625/625 [==============================] - 3s 6ms/step - loss: 0.2156 - accuracy: 0.9313 - val_loss: 0.3164 - val_accuracy: 0.8960
Epoch 6/10
625/625 [==============================] - 3s 6ms/step - loss: 0.2108 - accuracy: 0.9341 - val_loss: 0.3355 - val_accuracy: 0.8934
Epoch 7/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2052 - accuracy: 0.9366 - val_loss: 0.3354 - val_accuracy: 0.8944
Epoc[...]<jupyter_text>**방식 2: 바이그램 바이너리 인코딩** 바이그램(2-grams)을 유니그램 대신 이용해보자. 예를 들어 "the cat sat on the mat" 문장을 바이그램으로 처리하면 다음 단어주머니가 생성된다.```{"the", "the cat", "cat", "cat sat", "sat", "sat on", "on", "on the", "the mat", "mat"}````TextVectorization` 클래스의 `ngrams=N` 옵션을 이용하면N-그램들로 이루어진 어휘색인을 생성할 수 있다.<jupyter_code>text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="multi_hot",
)<jupyter_output><empty_output><jupyter_text>어휘색인 생성과 훈련셋, 검증셋, 테스트셋의 벡터화 과정은 동일하다.<jupyter_code>text_vectorization.adapt(text_only_train_ds)

binary_2gram_train_ds = train_ds.map(lambda x, y: (text_vectorization(x), y))
binary_2gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
binary_2gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))<jupyter_output><empty_output><jupyter_text>훈련 후 테스트셋에 대한 정확도가 90%를 조금 웃돌 정도로 많이 향상되었다.<jupyter_code>model = get_model()

callbacks = [
    keras.callbacks.ModelCheckpoint("binary_2gram.keras",
                                    save_best_only=True)
]

model.fit(binary_2gram_train_ds.cache(),
          validation_data=binary_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)

model = keras.models.load_model("binary_2gram.keras")
print(f"Test acc: {model.evaluate(binary_2gram_test_ds)[1]:.3f}")<jupyter_output>Epoch 1/10
625/625 [==============================] - 12s 18ms/step - loss: 0.3857 - accuracy: 0.8347 - val_loss: 0.2791 - val_accuracy: 0.9000
Epoch 2/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2592 - accuracy: 0.9082 - val_loss: 0.2947 - val_accuracy: 0.8988
Epoch 3/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2277 - accuracy: 0.9241 - val_loss: 0.3060 - val_accuracy: 0.8978
Epoch 4/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2074 - accuracy: 0.9333 - val_loss: 0.3417 - val_accuracy: 0.8994
Epoch 5/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2070 - accuracy: 0.9365 - val_loss: 0.3538 - val_accuracy: 0.8968
Epoch 6/10
625/625 [==============================] - 4s 6ms/step - loss: 0.1997 - accuracy: 0.9395 - val_loss: 0.3908 - val_accuracy: 0.8946
Epoch 7/10
625/625 [==============================] - 4s 6ms/step - loss: 0.1940 - accuracy: 0.9421 - val_loss: 0.3715 - val_accuracy: 0.8940
Epoc[...]<jupyter_text>**방식 3: 바이그램 TF-IDF 인코딩** N-그램을 벡터화할 때 사용 빈도를 함께 저장하는 방식을 사용할 수 있다.단어의 사용 빈도가 아무래도 문장 평가에 중요한 역할을 수행할 것이기 때문이다.아래 코드에서처럼 `output_mode="count"` 옵션을 사용하면 된다.<jupyter_code>text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="count"
)<jupyter_output><empty_output><jupyter_text>그런데 이렇게 하면 "the", "a", "is", "are" 등의 사용 빈도는 매우 높은 반면에"Chollet" 등의 단어는 빈도가 거의 0에 가깝게 나온다.또한 생성된 벡터의 대부분은 0으로 채워질 것이다. `max_tokens=20000`을 사용한 반면에 하나의 문장엔 많아야 몇 십개 정도의 단어만 사용되었기 때문이다. ```pythoninputs[0]: tf.Tensor([1. 1. 1. ... 0. 0. 0.], shape=(20000,), dtype=float32)``` 이 점을 고려해서 사용 빈도를 정규화한다. 평균을 원점으로 만들지는 않고 TF-IDF 값으로 나누기만 실행한다.이유는 평균을 옮기면 벡터의 대부분의 값이 0이 아니게 되어훈련에 보다 많은 계산이 요구되기 때문이다. **TF-IDF**의 의미는 다음과 같다.- `TF`(Term Frequency)    - 하나의 문장에서 사용되는 단어의 빈도    - 높을 수록 중요    - 예를 들어, 하나의 리뷰에 "terrible" 이 많이 사용되었다면        해당 리뷰는 부정일 가능성 높음.- `IDF`(Inverse Document Frequency)    - 데이터셋 전체 문장에서 사용된 단어의 빈도    - 낮을 수록 중요.     - "the", "a", "is" 등의 `IDF` 값은 높지만 별로 중요하지 않음.- `TF-IDF = TF / IDF` `output_mode="tf_idf"` 옵션을 사용하면 TF-IDF 인코딩을 지원한다.<jupyter_code>text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="tf_idf",
)<jupyter_output><empty_output><jupyter_text>훈련 후 테스트셋에 대한 정확도가 다시 89% 아래로 내려간다.여기서는 별 도움이 되지 않았지만 많은 텍스트 분류 모델에서는 1% 정도의 성능 향상을 가져온다.**주의사항**: 아래 코드는 현재(Tensorflow 2.6과 2.7) GPU를 사용하지 않는 경우에만 작동한다. 이유는 아직 모른다([여기 참조](https://github.com/fchollet/deep-learning-with-python-notebooks/issues/190)).<jupyter_code>text_vectorization.adapt(text_only_train_ds)

tfidf_2gram_train_ds = train_ds.map(lambda x, y: (text_vectorization(x), y))
tfidf_2gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
tfidf_2gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("tfidf_2gram.keras",
                                    save_best_only=True)
]
model.fit(tfidf_2gram_train_ds.cache(),
          validation_data=tfidf_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)

model = keras.models.load_model("tfidf_2gram.keras")
print(f"Test acc: {model.evaluate(tfidf_2gram_test_ds)[1]:.3f}")<jupyter_output>Model: "model_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_3 (InputLayer)        [(None, 20000)]           0         
                                                                 
 dense_4 (Dense)             (None, 16)                320016    
                                                                 
 dropout_2 (Dropout)         (None, 16)                0         
                                                                 
 dense_5 (Dense)             (None, 1)                 17        
                                                                 
=================================================================
Total params: 320,033
Trainable params: 320,033
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
625/625 [==============================] -[...]<jupyter_text>**부록: 문자열 벡터화 전처리를 함께 처리하는 모델 내보내기** 훈련된 모델을 실전에 배치하려면 텍스트 벡터화도 모델과 함께 내보내야 한다.이를 위해 `TextVectorization` 층의 결과를 재활용만 하면 된다.<jupyter_code>inputs = keras.Input(shape=(1,), dtype="string")
# 텍스트 벡터화 추가
processed_inputs = text_vectorization(inputs)
# 훈련된 모델에 적용
outputs = model(processed_inputs)

# 최종 모델
inference_model = keras.Model(inputs, outputs)<jupyter_output><empty_output><jupyter_text>`inference_model`은 일반 텍스트 문장을 직접 인자로 받을 수 있다.예를 들어 "That was an excellent movie, I loved it."라는 리뷰는긍정일 확률이 매우 높다고 예측된다.<jupyter_code>import tensorflow as tf

raw_text_data = tf.convert_to_tensor([
    ["That was an excellent movie, I loved it."],
])

predictions = inference_model(raw_text_data)
print(f"{float(predictions[0] * 100):.2f} percent positive")<jupyter_output>92.10 percent positive
{'fraction_non_alphanumeric': 0.14199024150231662, 'fraction_numerical': 0.03862396982246095, 'mean_word_length': 3.4049124074408526, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 98, 'https://': 8, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 21, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '25', 'dir': 'jupyter-structured-clean-dedup', 'id': '668544', 'license': 'MIT', 'n_tokens_mistral': 13387, 'n_tokens_neox': 15372, 'n_words': 2713, 'path': 'notebooks/dlp11_part01_introduction.ipynb', 'repo_name': 'codingalzi/dlp', 'size': '24389'}