이번 포스팅에서는 토큰화의 정의와 유형, 주요 알고리즘과 토크나이저를 구축할 때 주의할 사항과 평가지표에 대해서 소개하겠습니다. 

 

1. 토큰화(Tokenization)이란 무엇인가? 

2. 서브워드 토큰화 알고리즘 (Subword Tokenization Algorithms)에는 무엇이 있나? 

3. 토크나이저 구축 시 주의할 점은 무엇인가?  

4. 토크나이저의 성능을 평가하는 지표에는 무엇이 있나? 

5. Hugging Face의 Tokenizers 모듈로 BPE tokenizer 구현해보기

 

 

1. 토큰화(Tokenization)이란 무엇인가? 

 

NLP(자연어 처리)에서 토큰화(Tokenization)는 텍스트를 더 작은 단위로 나누는 과정을 의미합니다. 이 작은 단위들을 '토큰(token)'이라고 부릅니다. 토큰은 단어, 문자, 구, 문장, 서브워드(Subword) 등 다양한 기준으로 나눌 수 있으며, 토큰화의 목적은 주로 텍스트 데이터를 분석하기 용이하게 전처리하는 것입니다.  

주어진 문장 "토큰화는 자연어처리를 하는데 있어 핵심적인 작업입니다"에 대한 다양한 토큰화 예시는 다음과 같습니다.  

- 문장 단위 토큰화 (Sentence tokenization):  ["토큰화는 자연어처리를 하는데 있어 핵심적인 작업입니다"]  
- 단어 단위 토큰화 (Word tokenization):  ["토큰화는", "자연어처리를", "하는데", "있어", "핵심적인", "작업입니다"]  
- 서브워드 단위 토근화 (Subword tokenization):  [["토큰", "화는", "자연어", "처리를", "하는데", "있어", "핵심", "적인", "작업", "입니다"]  
- 문자 단위 토큰화 (Character Tokenization): 토", "콘", "화", "는", " ", "자", "연", "어", "처", "리", "를", " ", "하", "는", "데", " ", "있", "어", " ", "핵", "심", "적", "인", " ", "작", "업", "입", "니", "다"]  


특히, Subword 토큰화는 텍스트를 더 작은 의미 단위로 나누는 방법에 초점을 맞추며, 이는 언어의 모호성을 줄이고, 모르는 단어(out-of-vocabulary, OOV) 문제를 해결하며, 언어 모델의 크기를 줄이는 데 도움이 됩니다. 

 

 

2. 서브워드 토큰화 알고리즘 (Subword Tokenization Algorithms)에는 무엇이 있나?  


주요 Subword 토큰화 알고리즘으로는 BPE, WordPiece, SentencePiece, Unigram Language Model 이 있습니다.  

 

토큰화 알고리즘

* 이미지 출처: https://blog.floydhub.com/tokenization-nlp/

 

 


(1) Byte Pair Encoding (BPE): BPE는 데이터 압축 알고리즘에서 유래되었으며, 가장 빈번하게 등장하는 바이트 쌍(most frequent pair of bytes)을 반복적으로 병합하여 텍스트를 토큰화합니다. NLP에서는 문자열 쌍을 병합하여 사용되며, 이 과정을 통해 공통적으로 나타나는 문자열 패턴을 효율적으로 인코딩할 수 있습니다. 

(2) WordPiece: WordPiece는 BPE와 유사하지만, 병합 과정에서 언어 모델의 가능성(likelihood)을 최대화하는 쌍을 선택합니다. 이는 주로 Google의 BERT 모델에서 사용됩니다.  

(3) SentencePiece: SentencePiece는 언어에 구애받지 않고 원시 텍스트를 직접 토큰화할 수 있게 해주는 알고리즘으로, 미리 정의된 언어나 문법 규칙에 의존하지 않습니다. 이는 BPE나 WordPiece와 같은 알고리즘을 포함하여, 텍스트를 바이트 레벨(Byte-level)에서 처리함으로써 언어의 다양성에 대응할 수 있습니다. 

(4) Unigram Language Model: Unigram 언어 모델은 가능한 모든 서브워드 토큰의 집합에서 시작하여, 각 토큰을 제거하는 것이 언어 모델의 확률(Probability)을 최소한으로 감소시키는 방식으로 토큰을 선택합니다. 이 방법은 토큰의 확률 분포를 기반으로 하며, 최종적으로 가장 유용한 서브워드 집합을 결정합니다. 

이러한 Subword 토큰화 방법들은 특히 다양한 언어와 도메인에 걸쳐 효율적인 자연어 이해를 가능하게 하는 데 중요한 역할을 합니다. 각각의 방법은 특정 상황이나 요구에 맞게 선택되며, NLP 모델의 성능과 효율성을 크게 향상시킬 수 있습니다. 

 

 

 

3. 토크나이저 구축 시 주의할 점은 무엇인가?  

 

토크나이저의 구축(Build a Tokenizer)은 자연어 처리(NLP)에서 텍스트 데이터를 사전 정의된 단위(토큰)로 분리하는 프로그램이나 알고리즘을 개발하는 과정을 의미합니다. 이러한 토큰은 단어, 문자, 문장 또는 서브워드(subword) 등이 될 수 있으며, 토큰화 과정은 텍스트 분석, 기계 번역, 감정 분석 등 다양한 NLP 작업의 기초를 형성합니다.  

토크나이저 구축 시 주의할 점으로는 다음과 같은 것들이 있습니다.  

(1) 언어의 특성 이해: 처리하려는 언어의 구문적, 의미적 특성을 이해하는 것이 중요합니다. 예를 들어, 공백으로 단어를 구분하는 언어(예: 영어)와 달리, 공백 없이 문자가 연속되는 언어(예: 중국어)는 다른 접근 방식을 요구합니다.  

(2) 도메인 특성 고려: 텍스트 데이터가 속한 도메인(예: 의료, 법률, 일상 대화)에 따라 사용되는 언어가 다를 수 있습니다. 전문 용어나 구어체 표현을 적절히 처리할 수 있는 토크나이저를 구축해야 합니다.  

(3) 성능과 정확도: 토크나이저의 성능과 정확도는 다운스트림 NLP 작업의 결과에 직접적인 영향을 미칩니다. 높은 정확도로 토큰을 식별하면서도, 처리 시간이 너무 길지 않도록 효율성을 고려해야 합니다.  

(4) 확장성과 유연성: 언어는 시간이 지남에 따라 변하고, 새로운 단어나 표현이 등장합니다. 따라서, 토크나이저는 새로운 데이터나 도메인에 쉽게 적용할 수 있도록 확장 가능하고 유연해야 합니다.  

(5) 다양한 토큰화 기법 고려: 단어, 문장, 문자, 서브워드 토큰화 등 다양한 토큰화 기법 중에서 목적에 가장 적합한 방법을 선택해야 합니다. 때로는 여러 기법을 조합하여 사용하는 것이 더 효과적일 수 있습니다.  

(6) 사전 및 사후 처리: 토큰화 과정에서 발생할 수 있는 문제(예: 구두점 처리, 대소문자 구분, 불용어 제거 등)를 해결하기 위해 적절한 사전 처리(pre-processing) 및 사후 처리(post-processing) 단계를 고려해야 합니다.  

(7) 평가 및 테스트: 개발한 토크나이저의 성능을 정확히 평가하기 위해 다양한 데이터 세트와 시나리오에서 테스트를 수행해야 합니다. 이를 통해 토크나이저의 강점과 약점을 파악하고 개선할 수 있습니다.  

토크나이저를 구축할 때 이러한 주의 사항을 고려하면, 다양한 언어와 도메인에 걸쳐 효과적으로 작동하는 강력하고 유연한 토크나이저를 개발할 수 있습니다.  

 

 

4. 토크나이저의 성능을 평가하는 지표에는 무엇이 있나? 

 

토크나이저 성능을 측정하는 데 있어서 "subword fertility (서브워드 다산성)", "proportion of continued words (연속된 단어의 비율)", 그리고 "coverage (커버리지)" 지표 등이 있습니다.  


(1) 서브워드 다산성(Subword Fertility): 이 지표는 하나의 고유 단어가 몇 개의 서브워드로 분해되는지를 나타냅니다. 예를 들어, 토크나이저가 단어를 너무 많은 작은 조각으로 나눈다면, 다산성이 높다고 할 수 있습니다. 이는 특정 언어나 도메인에서 토크나이저가 얼마나 세밀하게 단어를 분해하는지를 보여주는 지표로, 너무 높은 다산성은 처리해야 할 토큰의 수가 증가하여 성능 저하를 일으킬 수 있습니다.  

(2) 연속된 단어의 비율(Proportion of Continued Words): 이는 원본 텍스트에서 단어가 서브워드로 분해될 때, 연속적으로 나타나는 서브워드 조각들의 비율을 나타냅니다. 예를 들어, 하나의 단어가 여러 서브워드로 나뉘어 처리될 경우, 이러한 서브워드들이 원본 단어의 의미를 얼마나 잘 유지하고 있는지를 평가할 수 있습니다. 높은 비율은 토크나이저가 단어의 구조를 잘 보존하고 있다는 것을 의미할 수 있습니다.  

(3) 커버리지(Coverage): 커버리지는 토크나이저가 주어진 텍스트 데이터셋 내의 단어나 표현을 얼마나 잘 포착하는지를 나타내는 지표입니다. 높은 커버리지는 토크나이저가 대부분의 단어나 표현을 정확하게 인식하고 처리할 수 있음을 의미하며, 이는 특히 다양한 언어나 도메인에 걸쳐 토크나이저의 범용성을 평가하는 데 유용합니다. 커버리지가 낮다면, 토크나이저가 일부 단어나 표현을 놓치거나 잘못 처리할 가능성이 있음을 나타냅니다.  

이러한 지표들은 토크나이저의 성능을 다각도로 평가하고, 특정 언어나 도메인에 대한 토크나이저의 적합성을 결정하는 데 도움을 줍니다. 

이러한 지표들로 토크나이저 성능을 평가할 때, 트레이드오프를 균형 있게 고려하는 것이 중요합니다.   

예를 들어 서브워드 다산성(Subword fertility)와 계속되는 단어의 비율이 높은 토크나이저는 보이지 않거나 드문 단어들을 인식 가능한 서브워드로 분해함으로써 효과적으로 처리할 수 있을지도 모릅니다. 그러나, 이는 처리 과정의 비효율성으로 이어질 수 있고, 모델이 원래 단어의 전체 맥락이나 의미를 포착하는 능력을 저해할 수도 있습니다.  

높은 커버리지(Coverage)는 일반적으로 바람직하며, 이는 토크나이저가 입력 텍스트를 자신의 어휘로 효과적으로 표현할 수 있음을 의미합니다. 그러나, 매우 높은 커버리지를 달성하기 위해서는 큰 어휘 크기가 필요할 수 있으며, 이는 모델의 복잡성과 계산 요구사항을 증가시킬 수 있습니다.  

요약하자면, 토크나이저의 선택과 그 매개변수의 최적화는 애플리케이션의 특정 요구사항, 처리되는 언어(들)의 특성, 그리고 계산 제약 조건을 고려하여 결정해야 합니다. 이러한 지표들을 균형 있게 고려함으로써, Downstream NLP 작업을 효율적으로 지원하는 토크나이저를 개발하는 데 도움이 될 수 있습니다.  

 

 

 

5. Hugging Face의 Tokenizers 모듈로 BPE tokenizer 구현해보기

 

먼저, 터미널에서 pip install로 Hugging Face의 tokenizers 모듈을 설치합니다. 

 

! pip install -q tokenizers

 

 

 

- tokenizers 모듈의 Tokenizer, BepTrainer 메소드로 BPE tokenizer 훈련하기

 

매개변수로 unk_token (Unknown token), special_tokens 를 등록해줍니다. 

 

## Train the BPE tokenizer
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace

# Initialize a tokenizer with BPE model
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()

# Initializer a trainer for the BPE model
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])

# Sample corpus for training the tokenizer
corpus = [
    "This is an example of how BPE tokenization works.", 
    "BPE is an algorithm for subword tokenization", 
    "BPE tokenization works by repeatedly merging the most frequent pair of characters.", 
]

# Train the tokenizer
tokenizer.train_from_iterator(corpus, trainer)

# tokenizer.train(files=["your_dataset.txt"], trainer=trainer) # for files


tokenizer
# <tokenizers.Tokenizer at 0x1d153181b70>

 

 

일단 tokenizer의 학습이 완료되면, 아래처럼 학습된 tokenizer의 속성값에 접근할 수 있습니다. 

 

 

- BPE tokenizer의 vocab_size 접근해서 가져오기: get_vocab_size()

 

# Accessing the vocabulary size of the tokenizer
vocab_size = tokenizer.get_vocab_size()
print("Vocabulary size:", vocab_size)

# Vocabulary size: 111

 

 

- 학습된 tokenizer의 Vocaburary 에 접근해서 가져오기: get_vocab()

 

# Accessing the vocaburary
vocab = tokenizer.get_vocab()

# The 'vocab' variable is a dictionary where keys are tokens and values are token ids
print(vocab)

# {'z': 33, 'atio': 45, 'ar': 62, 'edl': 69, 'P': 8, 'oken': 42, 'p': 24, 'ken': 
# 41, 'characters': 109, 'Th': 57, 'B': 6, 'E': 7, 'tokenization': 49, 'am': 61, '.': 
# 5, 'ter': 86, 'peat': 81, 'BPE': 46, 'pair': 105, '[UNK]': 0, 'k': 19, 'frequent': 110, 
# 'e': 14, 'y': 32, 'works': 56, 'or': 34, 'repeat': 88, 'fre': 71, 'ample': 93, '[MASK]': 4, 
# 'th': 55, 'aracter': 94, 'example': 97, 'ex': 67, 'most': 103, 'l': 20, 'ks': 52, 'an': 50, 
# 'r': 26, 'h': 17, 'io': 38, 'iz': 40, 'for': 70, 'subword': 106, 'repeatedly': 107, 'b': 11, 
# 'is': 39, 'tokenizatio': 48, 'o': 23, 'u': 29, 'frequ': 99, 'mo': 77, 'wor': 44, 'gi': 72, 
# 'character': 96, 'izatio': 47, 'f': 15, 'w': 30, 'm': 21, 'ai': 59, 'ent': 87, 'er': 51, 
# '[PAD]': 3, 'ithm': 102, 'ho': 74, 'n': 22, 'qu': 83, 'edly': 98, 'merging': 104, 'en': 36, 
# 'algorithm': 108, 'pai': 80, 're': 54, 'ac': 58, 'token': 43, 'BP': 37, 'how': 101, 's': 27, 
# 'bwor': 64, 'le': 76, 'by': 63, 'd': 13, 'al': 60, 'ng': 79, 'acter': 91, 'gor': 73, 't': 28, 
# 'at': 35, 'su': 85, 'T': 9, 'st': 84, 'x': 31, 'of': 53, 'q': 25, '[SEP]': 2, 'eat': 68, 
# 'dl': 66, 'This': 90, 'algor': 92, 'bword': 95, 'the': 89, 'ith': 75, '[CLS]': 1, 'ch': 65, 
# 'i': 18, 'a': 10, 'ple': 82, 'c': 12, 'mer': 78, 'ging': 100, 'g': 16}

 

 

- tokenizer의 {token_id: token} 쌍 가져오기: vocab.items() 를 역변환

 

# You can get "token_id: token" pairs by inverting the vocab dictionary
id_to_token = {id: token for token, id in vocab.items()}

print(id_to_token)
# {33: 'z', 45: 'atio', 62: 'ar', 69: 'edl', 8: 'P', 42: 'oken', 24: 'p', 41: 'ken', 
# 109: 'characters', 57: 'Th', 6: 'B', 7: 'E', 49: 'tokenization', 61: 'am', 5: '.', 
# 86: 'ter', 81: 'peat', 46: 'BPE', 105: 'pair', 0: '[UNK]', 19: 'k', 110: 'frequent', 
# 14: 'e', 32: 'y', 56: 'works', 34: 'or', 88: 'repeat', 71: 'fre', 93: 'ample', 4: '[MASK]', 
# 55: 'th', 94: 'aracter', 97: 'example', 67: 'ex', 103: 'most', 20: 'l', 52: 'ks', 50: 'an', 
# 26: 'r', 17: 'h', 38: 'io', 40: 'iz', 70: 'for', 106: 'subword', 107: 'repeatedly', 11: 'b', 
# 39: 'is', 48: 'tokenizatio', 23: 'o', 29: 'u', 99: 'frequ', 77: 'mo', 44: 'wor', 72: 'gi', 
# 96: 'character', 47: 'izatio', 15: 'f', 30: 'w', 21: 'm', 59: 'ai', 87: 'ent', 51: 'er', 
# 3: '[PAD]', 102: 'ithm', 74: 'ho', 22: 'n', 83: 'qu', 98: 'edly', 104: 'merging', 36: 'en', 
# 108: 'algorithm', 80: 'pai', 54: 're', 58: 'ac', 43: 'token', 37: 'BP', 101: 'how', 27: 's', 
# 64: 'bwor', 76: 'le', 63: 'by', 13: 'd', 60: 'al', 79: 'ng', 91: 'acter', 73: 'gor', 28: 't', 
# 35: 'at', 85: 'su', 9: 'T', 84: 'st', 31: 'x', 53: 'of', 25: 'q', 2: '[SEP]', 68: 'eat', 
# 66: 'dl', 90: 'This', 92: 'algor', 95: 'bword', 89: 'the', 75: 'ith', 1: '[CLS]', 65: 'ch', 
# 18: 'i', 10: 'a', 82: 'ple', 12: 'c', 78: 'mer', 100: 'ging', 16: 'g'}

 


- {token_id: token} 쌍을 token_id 기준으로 오름차순 정렬해서 보기

 

# Sort id_to_token dictionary by IDs
print(sorted(id_to_token.items(), key=lambda x: x[0]))

# [(0, '[UNK]'), (1, '[CLS]'), (2, '[SEP]'), (3, '[PAD]'), (4, '[MASK]'), (5, '.'), (6, 'B'), 
# (7, 'E'), (8, 'P'), (9, 'T'), (10, 'a'), (11, 'b'), (12, 'c'), (13, 'd'), (14, 'e'), (15, 'f'), 
# (16, 'g'), (17, 'h'), (18, 'i'), (19, 'k'), (20, 'l'), (21, 'm'), (22, 'n'), (23, 'o'), 
# (24, 'p'), (25, 'q'), (26, 'r'), (27, 's'), (28, 't'), (29, 'u'), (30, 'w'), (31, 'x'), 
# (32, 'y'), (33, 'z'), (34, 'or'), (35, 'at'), (36, 'en'), (37, 'BP'), (38, 'io'), (39, 'is'), 
# (40, 'iz'), (41, 'ken'), (42, 'oken'), (43, 'token'), (44, 'wor'), (45, 'atio'), (46, 'BPE'), 
# (47, 'izatio'), (48, 'tokenizatio'), (49, 'tokenization'), (50, 'an'), (51, 'er'), (52, 'ks'), 
# (53, 'of'), (54, 're'), (55, 'th'), (56, 'works'), (57, 'Th'), (58, 'ac'), (59, 'ai'), 
# (60, 'al'), (61, 'am'), (62, 'ar'), (63, 'by'), (64, 'bwor'), (65, 'ch'), (66, 'dl'), 
# (67, 'ex'), (68, 'eat'), (69, 'edl'), (70, 'for'), (71, 'fre'), (72, 'gi'), (73, 'gor'), 
# (74, 'ho'), (75, 'ith'), (76, 'le'), (77, 'mo'), (78, 'mer'), (79, 'ng'), (80, 'pai'), 
# (81, 'peat'), (82, 'ple'), (83, 'qu'), (84, 'st'), (85, 'su'), (86, 'ter'), (87, 'ent'), 
# (88, 'repeat'), (89, 'the'), (90, 'This'), (91, 'acter'), (92, 'algor'), (93, 'ample'), 
# (94, 'aracter'), (95, 'bword'), (96, 'character'), (97, 'example'), (98, 'edly'), 
# (99, 'frequ'), (100, 'ging'), (101, 'how'), (102, 'ithm'), (103, 'most'), (104, 'merging'), 
# (105, 'pair'), (106, 'subword'), (107, 'repeatedly'), (108, 'algorithm'), (109, 'characters'), 
# (110, 'frequent')]

 

 

- 토큰이 Vocabulary 에 들어있는지 확인하기: tokenizer.token_to_id(token)

 

# Checking if a token is in the vocabulary
token_id = tokenizer.token_to_id("BPE")

if token_id is not None:
    print("Token 'BPE' is in the vocabulary with ID:", token_id)
else:
    print("Token 'BPE' is not in the vocabulary")
    
# Token 'BPE' is in the vocabulary with ID: 46    


id_to_token[46]
# 'BPE'

 

 

- Sepcial Tokens 와 ID 확인하기

: special_tokens = ["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]

 

# Accessing Special Tokens and their IDs
# Assuming the tokenizer has been trained or loaded with special tokens
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]

for special_token in special_tokens:
    special_token_id = tokenizer.token_to_id(special_token)
    print(f"{special_token} token ID: {special_token_id}")
    
# [UNK] token ID: 0
# [CLS] token ID: 1
# [SEP] token ID: 2
# [PAD] token ID: 3
# [MASK] token ID: 4

 

 

- 새로운 텍스트 토큰화하기: tokenizer.encode(new text)

 

# Tokenize a new text
output = tokenizer.encode("This is a new example to showcase BPE tokenization.")

# Print the tokens and their IDs
print("Tokens:", output.tokens)
print("Token IDs:", output.ids)

# Tokens: ['This', 'is', 'a', 'n', 'e', 'w', 'example', 't', 'o', 's', 'how', 'c', 'a', 's', 'e', 'BPE', 'tokenization', '.']
# Token IDs: [90, 39, 10, 22, 14, 30, 97, 28, 23, 27, 101, 12, 10, 27, 14, 46, 49, 5]

 

 

- 토큰 IDs를 디코딩하기: tokenizer.decode(token_ids)

 

# Decode token IDs reversely
tokenizer.decode(output.ids)

# 'This is a n e w example t o s how c a s e BPE tokenization .'

 

 

- 새로운 토큰을 토크나이저에 추가하기: tokenizer.add_tokens(new_tokens)

 

# Adding new tokens to a tokenizer
# To add new tokens to a tokenizer using the tokenizers library by Hugging Face, you can use the add_tokens method if you're working with a tokenizer that supports dynamic vocabulary modifications. This feature is particularly useful when you have out-of-vocabulary (OOV) tokens that you want to include in your tokenizer's vocabulary for better model performance on specific tasks or domains.

# 1. Install the Tokenizers Library: First, ensure that the tokenizers library is installed. You can install it using pip if you haven't already.
# 2. Load or Initialize Your Tokenizer: Load the tokenizer you wish to modify. This could be a pre-trained tokenizer or a custom one you've created.
# 3. Add New Tokens: Use the add_tokens method to add your new tokens to the tokenizer's vocabulary. The method typically requires a list of new tokens to be added.


# [case 1] Add new tokens to the trained tokenizer above
new_tokens = ["SentencePiece", "Unigram"]
tokenizer.add_tokens(new_tokens)

# Verify if the new tokens were added
# Tokenize a new text
output = tokenizer.encode("SentencePiece Unigram")

# Print the tokens and their IDs
print("Tokens:", output.tokens)
print("Token IDs:", output.ids)

# Tokens: ['SentencePiece', 'Unigram']
# Token IDs: [111, 112]

 

 

- 새로운 토큰을 Hugging Face Hub에 있는 토크나이저에 추가하기: tokenizer.add_tokens(new_tokens)

 

# [case 2] adding new tokens to pretrained tokenizer and model from Hugging Face Hub
# Let's see how to increase the vocabulary of Bert model and tokenizer
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased")

num_added_toks = tokenizer.add_tokens(["new_tok1", "my_new-tok2"])
print("We have added", num_added_toks, "tokens")

# Notice: resize_token_embeddings expect to receive the full size of the new vocabulary, i.e., the length of the tokenizer.
model.resize_token_embeddings(len(tokenizer))

 

 

- 토크나이저를 직렬화, 역직렬화 (serialization and deserialization of tokenizer)

 

# Serialization and Deserialization
# Serialize tokenizer to string
tokenizer_str = tokenizer.to_str()

print(tokenizer_str)

# {"version":"1.0",
# "truncation":null,
# "padding":null,
# "added_tokens":[{"id":0,"content":"[UNK]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true},
# {"id":1,"content":"[CLS]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true},{"id":2,"content":"[SEP]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true},{"id":3,"content":"[PAD]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true},{"id":4,"content":"[MASK]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}],"normalizer":null,
# "pre_tokenizer":{"type":"Whitespace"},
# "post_processor":null,"decoder":null,
# "model":{"type":"BPE","dropout":null,"unk_token":"[UNK]","continuing_subword_prefix":null,"end_of_word_suffix":null,"fuse_unk":false,
# "vocab":{"[UNK]":0,"[CLS]":1,"[SEP]":2,"[PAD]":3,"[MASK]":4,".":5,"B":6,"E":7,"P":8,"T":9,"a":10,"b":11,"c":12,"d":13,"e":14,"f":15,"g":16,"h":17,"i":18,"k":19,"l":20,"m":21,"n":22,"o":23,"p":24,"q":25,"r":26,"s":27,"t":28,"u":29,"w":30,"x":31,"y":32,"z":33,"or":34,"at":35,"en":36,"BP":37,"io":38,"is":39,"iz":40,"ken":41,"oken":42,"token":43,"wor":44,"atio":45,"BPE":46,"izatio":47,"tokenizatio":48,"tokenization":49,"an":50,"er":51,"ks":52,"of":53,"re":54,"th":55,"works":56,"Th":57,"ac":58,"ai":59,"al":60,"am":61,"ar":62,"by":63,"bwor":64,"ch":65,"dl":66,"ex":67,"eat":68,"edl":69,"for":70,"fre":71,"gi":72,"gor":73,"ho":74,"ith":75,"le":76,"mo":77,"mer":78,"ng":79,"pai":80,"peat":81,"ple":82,"qu":83,"st":84,"su":85,"ter":86,"ent":87,"repeat":88,"the":89,"This":90,"acter":91,"algor":92,"ample":93,"aracter":94,"bword":95,"character":96,"example":97,"edly":98,"frequ":99,"ging":100,"how":101,"ithm":102,"most":103,"merging":104,"pair":105,"subword":106,"repeatedly":107,"algorithm":108,"characters":109,"frequent":110},
# "merges":["o r","a t","e n","B P","i o","i s","i z","k en","o ken","t oken","w or","at io","BP E","iz atio","token izatio","tokenizatio n","a n","e r","k s","o f","r e","t h","wor ks","T h","a c","a i","a l","a m","a r","b y","b wor","c h","d l","e x","e at","e dl","f or","f re","g i","g or","h o","i th","l e","m o","m er","n g","p ai","p eat","p le","q u","s t","s u","t er","en t","re peat","th e","Th is","ac ter","al gor","am ple","ar acter","bwor d","ch aracter","ex ample","edl y","fre qu","gi ng","ho w","ith m","mo st","mer ging","pai r","su bword","repeat edly","algor ithm","character s","frequ ent"]}}

 

 

- 토크나이저를 저장하기 로딩하기: tokenizer.save(), tokenizer.from_file()

 

## Saving and Loading tokenizer

# Save it to a file
tokenizer.save("./tokenizer.json")


import os

print("Current working directory:", os.getcwd())
print("File list at current directory:", os.listdir(os.getcwd()))

# Current working directory: C:\Users\Documents\BPE-Tokenizer
# File list at current directory: ['BPE-subword-tokenization.ipynb', 'tokenizer.json']



# Load the saved Tokenizer JSON file back
tokenizer = tokenizer.from_file("./tokenizer.json")

print("Vocabulary size:", tokenizer.get_vocab_size())

# Vocabulary size: 111

 

 

 

- Hugging Face Hub에서 AutoTokenizer 메소드를 사용해 이미 훈련이 된 모델의 tokenizer 사용하기 

 

# Using Pre-trained tokenization from the Hugging Face Hub
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# Accessing tokenizer attributes
vocab_size = tokenizer.vocab_size
print("Vocabulary size:", vocab_size)

# Vocabulary size: 30522


# Special tokens
print("PAD token:", tokenizer.pad_token)
print("PAD token ID:", tokenizer.pad_token_id)

# PAD token: [PAD]
# PAD token ID: 0

 

 

[ Reference ]

1. "Tokenizers: How Machines Read" by Cathal Horan: https://blog.floydhub.com/tokenization-nlp/

2. "Summary of the tokenizers" from Hugging Face: 
https://huggingface.co/docs/transformers/tokenizer_summary

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

 

728x90
반응형
Posted by Rfriend
,

1. 언어모델 평가 지표 Perplexity 란 무엇인가?

 

Perplexity는 자연어 처리(NLP)에서 확률적 또는 통계적 모델의 품질을 평가하는 데 사용되는 척도입니다. 특히 언어 모델(Language Model)의 품질을 평가할 때 사용됩니다. Perplexity는 언어 모델이 샘플을 얼마나 잘 예측하는지를 정량화합니다. Perplexity 점수가 낮을수록 모델이 샘플을 예측하는 데 더 나은 것으로 간주됩니다.  

더 기술적인 용어로, Perplexity는 모델이 예측을 할 때 얼마나 "당황"하거나 "혼란"을 겪는지를 측정합니다. 이는 모델에 따른 테스트 세트의 엔트로피(또는 평균 로그 가능도, average log-likelihood)의 지수로 계산됩니다. 언어 모델의 경우, 이는 본질적으로 모델이 다음 토큰(예: 문장에서의 단어)을 예측할 때 평균적으로 가지고 있다고 생각하는 선택의 수(the average number of choices), 분기 수 (number of branches)를 평가하는 것을 의미합니다.  

 

Perplexity - Language Model Evaluation Metric

 

- N: 총 데이터 개수

- t_nk: n개째 데이터의 k번째 값 (정답이면 1, 정답이 아니면 0)

- y_nk: 모델이 예측한 확률값 (0~1 사이의 실수 값)

 

 

만약 데이터 수가 한 개라면 언어 모델이 예측한 다음 단어의 확률의 역수 (inverse of the probability) 가 Perplexity 가 됩니다.  즉, 정답을 높은 확률로 예측을 제대로 했으면 이 확률의 역수인 Perplexity는 낮은 값을 가지고 됩니다. 


언어 모델의 맥락에서, 낮은 Perplexity를 가진 모델은 테스트 데이터를 예측하는 데 더 정확하다고 간주됩니다. 이는 모델이 테스트 세트에 존재하는 언어 패턴을 더 잘 이해하고 있다는 것을 나타내며, 다른 모델을 비교하거나 모델의 성능을 향상시키기 위해 모델의 파라미터를 조정하는 데 유용한 지표가 됩니다.  

 

 

2. Perplexity 예시

 

이 NLP 맥락에서의 Perplexity를 설명하기 위한 매우 간단한 예를 들어보겠습니다. 시퀀스에서 다음 단어를 예측하는 작업을 맡은 언어 모델이 있다고 가정해 보겠습니다. 매우 작은 테스트 세트에서 모델의 성능을 평가하는 데 Perplexity를 어떻게 사용할 수 있는지 살펴보겠습니다.  


테스트 세트가 단 하나의 문장으로 구성되어 있다고 상상해 보겠습니다: 

 

"The cat sat on the ___", 우리는 모델이 빈칸에 들어갈 단어를 예측하기를 원합니다. 간단하게 하기 위해, 모델이 빈칸을 채우기 위해 고려하는 가능한 단어가 네 개 있다고 가정합시다. 

{mat, hat, cat, bat}  

 

2-1. 성능이 우수한 언어 모델의 예


이제, 우리의 언어 모델이 이 단어들에 다음과 같은 확률을 할당한다고 가정해 보겠습니다.  

- P(mat) = 0.8
- P(hat) = 0.1
- P(cat) = 0.07
- P(bat) = 0.03

 

Good language model - probabilities



이 확률을 바탕으로, 우리는 이 테스트 케이스에 대한 모델의 Perplexity를 계산할 수 있습니다. Perplexity는 exp(Cross-Entropy Loss)로 정의되며, 여기서 p(x)는 모델이 올바른 단어에 할당한 확률입니다. 간단하게 하기 위해, "mat"이 올바른 다음 단어라고 가정합시다. 이 단일 단어 예측에 대한 Perplexity는 "mat"의 확률만을 사용하여 계산될 것입니다, 왜냐하면 그것이 올바른 단어이기 때문입니다. 

계산은 다음과 같습니다. 

Perplexity = exp(-log(0.8)) = 1.25

올바른 단어 "mat"에 대해 모델이 0.8의 확률을 예측한 이 매우 단순화된 예제에서의 Perplexity는 대략 1.25입니다.  

이는 평균적으로 모델이 1.25개의 동등하게 가능성 있는 단어 중에서 균등하게 올바르게 선택해야 하는 것처럼 혼란스러워한다는 것을 의미합니다. Perplexity가 낮을수록 더 나은 예측 성능을 나타내므로, 이 맥락에서 1.25의 Perplexity는 이 매우 단순화된 시나리오에서 다음 단어를 예측하는 데 있어 모델이 상대적으로 좋다는 것을 의미합니다.  

 


2-2. 성능이 낮은 언어 모델의 예

 

아래에는 성능이 낮은 언어 모델이 "The cat sat on the ___"  다음에 나올 단어를 예측한 단어별 확률입니다. 

 

{mat, hat, cat, bat}

 

- P(mat) = 0.1
- P(hat) = 0.5
- P(cat) = 0.3
- P(bat) = 0.1

 

Bad language model - probabilities

 

 

 

이때 정답은 mat 이라고 했을 때, 이 언어 모델의 Perplexity는 

 

Perplexity(mat) = exp(-log(0.1)) = 10.0

 

이 됩니다. 즉, 위의 성능 좋은 언어 모델은 정답인 mat을 높은 확률로 예측했고 Perplexity가 1.25 였던 반면에, 이번에 성능이 낮은 언어 모델은 정답인 mat이 다음에 나올 확률을 0.1 로 매우 낮게 예측을 해서 Perplexity가 10.0 으로 높게 나왔습니다. 

 

 

[ Reference ]

- Perplexity from Wikipedia: https://en.wikipedia.org/wiki/Perplexity 

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 여러개의 pdf 파일을 참조하여 ChatGPT로 질의응답하는 방법을 소개하겠습니다. 

RAG (Retrieval0Augmented Generation) 방법을 사용한 답변 생성입니다. 

 

1. pdf 파일을 로딩하고 임베딩으로 변환하여 Chroma Vector DB에 저장하고 인덱싱하기

2. 사용자 질문과 문서의 임베딩에 대해 Semantic Search하여 가장 유사한 문서 찾기

3. Semantic Search로 찾은 문서를 사용자 질문과 함께 ChatGPT에 보내서 답변 생성하기

4. 다른 pdf 파일을 로딩하고 임베딩으로 변환하여 Vector DB에 추가하기

5. 대화 기록에 대한 기억 (Chat History Memory)을 사용하여 ChatGPT 답변 생성하기

 

 

LangChain, ChatGPT, pdf loader, Chroma Vector DB, Memory Storage

 

* source: https://www.datascienceengineer.com/blog/post-multiple-pdfs-with-gpt

 

 

먼저, 터미널에서 pip install 로 openai, langchain, pypdf, chroma 모듈을 설치합니다. 

 

! pip install -q openai langchain pypdf chroma

 

 

 

1. pdf 파일을 로딩하고 임베딩으로 변환하여 Chroma Vector DB에 저장하고 인덱싱하기

 

아래에 필요한 모듈을 importing 합니다. 

 

# Import the required modules
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA, ConversationalRetrievalChain

 

 

환경변수로 OpenAI의 ChatGPT를 사용하기 위한 API Key를 등록합니다. 

 

# Set OpenAI API Key as an environment variable
import os

os.environ["OPENAI_API_KEY"]="sk-xxxx" # set with yours

 

 

LangChain 에서는 PDF (Portable Document Format) 파일을 로드할 수 있는 여러가지 방법을 제공합니다. 그중에서도 PyPDF 모듈을 이용해서 PDF 파일을 로드해보겠습니다. 

 

# Load a PDF file, using the LangChain PyPDF loader
loader = PyPDFLoader("LangChain.pdf")

 

 

위에서 pdf 파일 문서를 로딩한 다음에는 RecursiveCharacterTextSplitter() 메소드를 사용해서 최대한 문장(sentence)를 유지한 상태의 작은 chunk 로 분할해줍니다. chuck_size, chunk_overlap 을 매개변수로 정의해줄 수 있습니다. 

 

# Split the text in chunks, using LangChain Recursive Character Text Splitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
    )

pages = loader.load_and_split(text_splitter)

 

 

pdf 로드 후 분할을 하면 chunk 의 텍스트인 page_content, 그리고 소스 파일과 페이지 번호가 있는 metadata 에 각각 접근할 수 있습니다. 

 

pages[0].page_content
# LangChain Introduction
# LangChain  is a framework for developing applications powered by language models.
# .... 
#  LangServe : A library for deploying LangChain chains as a REST API.
#  LangSmith : A developer platform that lets you debug, test, evaluate, and


pages[0].metadata
# {'source': 'LangChain.pdf', 'page': 0}

 

 

OpenAI의 Text Embedding 을 이용하여 chunk의 문서들을 임베딩 벡터로 변환하고, Chroma Vector DB에 저장하고 인덱싱합니다. 이때 메모리에만 남겨두는 것이 아니라 directory에 영구저장(persist)하여 추후 재사용할 수 있도록 합니다. 

 

# Create a persistent, file-based vector store, using Chroma vector store.
directory = 'index_store'
vector_index = Chroma.from_documents(
    pages, # Documents
    OpenAIEmbeddings(), # Text embedding model
    persist_directory=directory # persists the vectors to the file system
    )

vector_index.persist()

 

 

Chroma Vector DB에 대한 자세한 내용은 https://www.trychroma.com/ 를 참고하세요. 

 

Chroma Vector DB

 

 

2. 사용자 질문과 문서의 임베딩에 대해 Semantic Search하여 가장 유사한 문서 찾기

 

위의 1번에서 pdf를 로딩하고 분할 후 임베딩 벡터로 변환하여 저장한 문서와 사용자 질문을 임베딩으로 변환한 벡터 간의 cosine similarity를 계산하여, 가장 유사한 k개의 문서를 검색 (Semantic Search)합니다. 

 

LangChain 에서 제공하는 Vector store-backed retriever 에는 MMR (Maximum marginal relevance) retriever와 Similarity retriever 가 있습니다. 

 

# Create the retriever and the query-interface. 
retriever = vector_index.as_retriever(
    search_type="similarity", # Cosine Similarity
    search_kwargs={
        "k": 3, # Select top k search results
    } 
)

 

 

위에서 정의한 Similarity retriever 를 사용하여 get_relevant_documents() 로 질문을 던져서 검색을 하여 질문과 가장 유사한 k개의 문서를 추출해보겠습니다. 

 

retriever.get_relevant_documents("What is VectorStore for Retreival in LangChain?")

[Out]
[Document(page_content='allowing you to choose the one best suited for your needs. LangChain provides a \nstandard interface, allowing you to easily swap between models.  \nVector stores  \nWith the rise of embeddings, there has emerged a need for databases to support \nefficient storage and searching of these embeddings. LangChain provides \nintegrations with over 50 different vecto rstores, from open -source local ones to \ncloud -hosted proprietary ones, allowing you to choose the one best suited for your \nneeds. LangChain exposes a standard interface, allowing you to easily swap between \nvector stores.  \nRetrievers  \nOnce the data is in the database, you still need to retrieve it. LangChain supports \nmany different retrieval algorithms and is one of the places where we add the most \nvalue. LangChain supports b asic methods that are easy to get started - namely \nsimple semantic search. However, we have also added a collection of algorithms on \ntop of this to increase performance. These include:', metadata={'page': 2, 'source': 'LangChain.pdf'})]

 

 

 

 

3. Semantic Search로 찾은 문서를 사용자 질문과 함께 ChatGPT에 보내서 답변 생성하기

 

LangChain에 제공하는 Retrieval 기반 QA 클래스에는 RetrievalQA와 ConversationalRetrievalChain 의 두가지 종류가 있습니다. 

 

- RetreivalQA: Retrieval 만을 사용하여 답변 생성

- ConversationalRetrievalChain: Chat History 기억과 Retrieval 을 같이 사용하여 답변 생성

 

먼저, RetrievalQA 클래스를 사용하여 ChatGPT에 Retrieval 만을 이용한 질의응답을 해보겠습니다. 아래 예에서는 temperature=0 으로 설정해서 보수적이고 일관적인 답변을 생성하도록 했으며, LLM 모델로는 ChatGPT-4 버전을 사용해보겠습니다. 그리고 return_source_documents=True 로 설정하여 답변 생성에 참고한 소스 문서를 같이 반환하도록 하여 나중에 사용자가 확인할 수 있도록 하였습니다. 

 

# Create the chain for allowing us to chat with the document.
qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(temperature=0, model="gpt-4"), 
    chain_type="stuff", 
    retriever=retriever,
    return_source_documents=True # source document which were used as source files
)

 

 

위에서 작성한 qa_chain 에 대해 invoke() 메소드를 사용해서 사용자 질문을 던지면, ChatGPT는 답변 결과(result)와 답변을 생성할 때 참고했던 소스 문서(source_documents)를 반환합니다. 

 

# Query the pdf file
qa_chain.invoke("What is VectorStore for Retrieval in LangChain")

# {'query': 'What is VectorStore for Retrieval in LangChain',
#  'result': 'VectorStore for Retrieval in LangChain refers to the feature that allows LangChain to integrate with over 50 different vector stores. These vector stores can be open-source local ones or cloud-hosted proprietary ones. The VectorStore feature in LangChain allows users to efficiently store and search embeddings, which are representations of textual data in a numerical format. This feature provides flexibility for users to choose the vector store that best suits their needs. LangChain also provides a standard interface that allows users to easily swap between different vector stores.',
#  'source_documents': [Document(page_content='allowing you to choose the one best suited for your needs. LangChain provides a \nstandard interface, allowing you to easily swap between models.  \nVector stores  \nWith the rise of embeddings, there has emerged a need for databases to support \nefficient storage and searching of these embeddings. LangChain provides \nintegrations with over 50 different vecto rstores, from open -source local ones to \ncloud -hosted proprietary ones, allowing you to choose the one best suited for your \nneeds. LangChain exposes a standard interface, allowing you to easily swap between \nvector stores.  \nRetrievers  \nOnce the data is in the database, you still need to retrieve it. LangChain supports \nmany different retrieval algorithms and is one of the places where we add the most \nvalue. LangChain supports b asic methods that are easy to get started - namely \nsimple semantic search. However, we have also added a collection of algorithms on \ntop of this to increase performance. These include:', metadata={'page': 2, 'source': 'LangChain.pdf'}),
#   Document(page_content='allowing you to choose the one best suited for your needs. LangChain provides a \nstandard interface, allowing you to easily swap between models.  \nVector stores  \nWith the rise of embeddings, there has emerged a need for databases to support \nefficient storage and searching of these embeddings. LangChain provides \nintegrations with over 50 different vecto rstores, from open -source local ones to \ncloud -hosted proprietary ones, allowing you to choose the one best suited for your \nneeds. LangChain exposes a standard interface, allowing you to easily swap between \nvector stores.  \nRetrievers  \nOnce the data is in the database, you still need to retrieve it. LangChain supports \nmany different retrieval algorithms and is one of the places where we add the most \nvalue. LangChain supports b asic methods that are easy to get started - namely \nsimple semantic search. However, we have also added a collection of algorithms on \ntop of this to increase performance. These include:', metadata={'page': 2, 'source': 'LangChain.pdf'}),
#   Document(page_content='\uf0b7 And more!  \nIndexing  \nThe LangChain  Indexing API  syncs your data from any source into a vector store, \nhelping you:  \n\uf0b7 Avoid writing duplicated content into the vector store  \n\uf0b7 Avoid re -writing unchanged content  \n\uf0b7 Avoid re -computing embeddings over unchanged content  \nAll of which should save you time and money, as well as improve  your vector search \nresults.', metadata={'page': 2, 'source': 'LangChain.pdf'})]}

 

 

 

4. 다른 pdf 파일을 로딩하고 임베딩으로 변환하여 Vector DB에 추가하기

 

위의 1~3에서 사용했던 pdf 문서와는 다른 pdf 문서를 추가로 로딩, 분할, 임베딩 변환, Vector DB에 저장, 인덱싱하여, 여러개의 pdf 문서들을 참고하여 질의응답을 할 수 있습니다. 

 

vector_index.add_documents() 로 기존의 Chroma Vector DB에 임베딩 변환, 저장해주고, persist() 로 파일 기반의 Vector Store에 영구저장할 수 있습니다. 

 

# Adding additional documents to vector store
# Load a PDF file, using the LangChain PyPDF loader
loader = PyPDFLoader("Chain-of-Thought-prompting.pdf")

# Split the text in chunks, using LangChain Recursive Character Text Splitter
pages_new = loader.load_and_split(text_splitter)

# Add additional documents to vector store
_ = vector_index.add_documents(pages_new)

# Create a persistent, file-based vector store, using Chroma vector store.
vector_index.persist()

 

 

그러면 qa_chain.invoke() 로 기존 chain에 대해 사용자 질의를 했을 때 위의 4번에서 새로 추가한 pdf 문서의 내용까지 검색, 참조하여 답변을 생성해줍니다. (아래 예제에서는 두번째로 추가한 Chain-of-Thought-prompting.pdf 파일을 참조해서 적절한 답변을 생성했음) 

 

# Query the pdf file
qa_chain.invoke("What is Chain-of-Thought Prompting?")

# {'query': 'What is Chain-of-Thought Prompting?',
#  'result': 'Chain-of-Thought Prompting is a method used to improve the reasoning abilities of large language models. It involves generating a series of intermediate reasoning steps, called a chain of thought, and using these demonstrations as prompts for the language model. The idea is that by providing examples of how to reason through a problem, the language model can better perform complex reasoning tasks. This approach has shown promising results in improving performance on arithmetic, commonsense, and symbolic reasoning tasks.',
#  'source_documents': [Document(page_content='C Extended Related Work\nChain-of-thought prompting is a general approach that is inspired by several prior directions: prompt-\ning, natural language explanations, program synthesis/execution, numeric and logical reasoning, and\nintermediate language steps.\nC.1 Prompting\nThe recent success of large-scale language models has led to growing interest in improving their\ncapability to perform tasks via prompting (Brown et al. (2020), and see Liu et al. (2021) for a\nsurvey). This paper falls in the category of general prompting approaches, whereby input prompts are\noptimized to allow a single large language model to better perform a variety of tasks (Li and Liang,\n2021; Lester et al., 2021; Reif et al., 2022, inter alia ).\nOne recent line of work aims to improve the ability of language models to perform a task by providing\ninstructions that describe the task (Raffel et al., 2020; Wei et al., 2022a; Ouyang et al., 2022; Sanh', metadata={'page': 23, 'source': 'Chain-of-Thought-prompting.pdf'}),
#   Document(page_content='Chain-of-Thought Prompting Elicits Reasoning\nin Large Language Models\nJason Wei Xuezhi Wang Dale Schuurmans Maarten Bosma\nBrian Ichter Fei Xia Ed H. Chi Quoc V . Le Denny Zhou\nGoogle Research, Brain Team\n{jasonwei,dennyzhou}@google.com\nAbstract\nWe explore how generating a chain of thought —a series of intermediate reasoning\nsteps—significantly improves the ability of large language models to perform\ncomplex reasoning. In particular, we show how such reasoning abilities emerge\nnaturally in sufficiently large language models via a simple method called chain-of-\nthought prompting , where a few chain of thought demonstrations are provided as\nexemplars in prompting.\nExperiments on three large language models show that chain-of-thought prompting\nimproves performance on a range of arithmetic, commonsense, and symbolic\nreasoning tasks. The empirical gains can be striking. For instance, prompting a\nPaLM 540B with just eight chain-of-thought exemplars achieves state-of-the-art', metadata={'page': 0, 'source': 'Chain-of-Thought-prompting.pdf'}),
#   Document(page_content='2021). In this paper, we combine the strengths of these two ideas in a way that avoids their limitations.\nSpecifically, we explore the ability of language models to perform few-shot prompting for reasoning\ntasks, given a prompt that consists of triples: ⟨input, chain of thought , output⟩. Achain of thought is\na series of intermediate natural language reasoning steps that lead to the final output, and we refer to\nthis approach as chain-of-thought prompting . An example prompt is shown in Figure 1.\nWe present empirical evaluations on arithmetic, commonsense, and symbolic reasoning benchmarks,\nshowing that chain-of-thought prompting outperforms standard prompting, sometimes to a striking\ndegree. Figure 2 illustrates one such result—on the GSM8K benchmark of math word problems\n(Cobbe et al., 2021), chain-of-thought prompting with PaLM 540B outperforms standard prompting\nby a large margin and achieves new state-of-the-art performance. A prompting only approach is', metadata={'page': 1, 'source': 'Chain-of-Thought-prompting.pdf'})]}

 

 

 

5. 대화 기록에 대한 기억 (Chat History Memory)을 사용하여 ChatGPT 답변 생성하기

 

마지막으로 ConversationalRetrievalChain 클래스를 사용해서 '대화 내역에 대한 기억 (Chat History Memory) + Retrieval' 을 동시에 고려하여 사용자 질의에 대한 답변을 생성해보겠습니다. 

 

## Adding memory to conversations
# Instead of the RetrievalQA chain, we'll use the ConversationalRetrievalChain. 
# ConversationalRetrievalChain allows to seamlessly add historical context or memory to chain.
conv_chain = ConversationalRetrievalChain.from_llm(
    ChatOpenAI(temperature=0, model="gpt-4"), 
    retriever=retriever,
)

 

 

chat_history 리스트를 만들어주고, conv_chain.invoke() 로 질의응답을 할 때마다 {"chat_history": chat_history} 에 질문과 답변의 대화 내역을 차곡차곡 저장(chat_history.append((query, result["result"])))해줍니다. 

 

# Initialize our chat history. 
chat_history = []
query = "What is LangChain?"
result = conv_chain.invoke({"question": query, "chat_history": chat_history})

print(result["answer"])

# 'LangChain is a framework for developing applications powered by language models. 
# It enables applications to be context-aware and reason based on the provided context. 
# The framework consists of several parts including LangChain Libraries, LangChain Templates, 
# LangServe, and LangSmith. The libraries contain interfaces and integrations 
# for various components, a basic runtime for combining these components into chains 
# and agents, and off-the-shelf implementations of chains and agents. 
# The templates are a collection of easily deployable reference architectures 
# for a wide variety of tasks. LangServe is a library for deploying LangChain chains as a REST API,
# and LangSmith is a developer platform for debugging, testing, and evaluating.'


# Add previous conversation to chat history
chat_history.append((query, result["answer"]))

 

 

이러면 질의응답 기억을 참고하여 새로운 답변을 생성할 수 있습니다. 아래 예에서는 위의 답변을 줄여서 요약하라고 지시했습니다. 

 

# Query to shorten the last sentence
query = "Can you shorten the answer above?"
result = conv_chain.invoke({"question": query, "chat_history": chat_history})

print(result["answer"])

# 'LangChain is a framework used for developing applications that are powered by language models. 
# It allows these applications to be context-aware and capable of reasoning based on provided context. 
# The framework includes Python and JavaScript libraries, deployable templates for various tasks, 
# a library for deploying as a REST API, and a developer platform for debugging, testing, and evaluation.'

 

 

질문과 답변을 나중에 참조할 수 있도록 chat_history.append((query, result["answer"])) 로 기억으로 저장해둡니다. 

 

chat_history.append((query, result["answer"]))


chat_history

# [('What is LangChain?',
#   'LangChain is a framework for developing applications powered by language models. It enables applications to be context-aware and reason based on the provided context. The framework consists of several parts including LangChain Libraries, LangChain Templates, LangServe, and LangSmith. The libraries contain interfaces and integrations for various components, a basic runtime for combining these components into chains and agents, and off-the-shelf implementations of chains and agents. The templates are a collection of easily deployable reference architectures for a wide variety of tasks. LangServe is a library for deploying LangChain chains as a REST API, and LangSmith is a developer platform for debugging, testing, and evaluating.'),
#  ('Can you shorten the answer above?',
#   'LangChain is a framework used for developing applications that are powered by language models. It allows these applications to be context-aware and capable of reasoning based on provided context. The framework includes Python and JavaScript libraries, deployable templates for various tasks, a library for deploying as a REST API, and a developer platform for debugging, testing, and evaluation.')]

 

 

 

[ Reference ]

* How to use LangChain and GPT to ask questions on multiple pdf documents:
https://www.datascienceengineer.com/blog/post-multiple-pdfs-with-gpt

* LangChain - Document Loaders - PDF: 
https://python.langchain.com/docs/modules/data_connection/document_loaders/pdf

* OpenAI Text Embedding: 

https://platform.openai.com/docs/guides/embeddings

* Chroma - AI-native open-source embedding database: 
https://www.trychroma.com/

* Vector store-backed retriever: 
https://python.langchain.com/docs/modules/data_connection/retrievers/vectorstore

* 위 실습에 사용한 pdf 파일: LangChain.pdf, Chain-of-Thought-prompting.pdf

LangChain.pdf
0.28MB
Chain-of-Thought-prompting.pdf
0.85MB

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. 

 

728x90
반응형
Posted by Rfriend
,

데이터, 정보가 저장되고 공유되고 유통되는 채널 중에 Web 이 있습니다. LangChain과 LLM 모델을 사용해서 텍스트를 생성할 때 Web URL 로 부터 데이터를 로드해서 Context 정보로 LLM 모델에 제공하여 이를 참조해 최종 답변을 생성하게 할 수 있습니다. 

 

요즘에 LLM을 이용한 서비스 중에 보면, 특정 웹 서비스의 최상위 URL을 입력하면 하위 directory의 모든 웹 페이지 데이터를 자동으로 크롤링해와서, 이들 모든 웹페이지의 정보를 참조하여 사용자의 질문에 답변을 생성하는 RAG(Retrieval Augmented Generation) 방법을 적용한 챗봇을 매우 짧은 시간에 편리하게 해주는 만들어주는 서비스가 있습니다. 이런 아이디어를 쉽게 구현할 수 있는 방법이 있습니다. 

 

이번 포스팅에서는 LangChain을 이용하여 특정 Root Directory를 지정해주면 그 밑의 모든 URL에 있는 텍스트 데이터를 로드해서 LLM 모델의 인풋으로 사용할 수 있도록 준비하는 방법을 소개하겠습니다. 

 

LangChain - Recursive URL Loader

 

 

먼저, 터미널에서 pip install을 이용해서 필요한 Python 모듈을 설치합니다. 

 

! pip install langchain openai unstructured libmagic python-magic-bin

 

 

LangChain의 RecursiveUrlLoader 를 이용하면 Root directory 밑의 모든 child pages 에 대해서 자동으로 재귀적으로 (Recursive) 순회하면서 각 웹페이지에 있는 텍스트를 가져와서 로딩해줍니다. 

 

RecursiveUrlLoader 의 'exclude_dirs' 매개변수를 이용하면 제외를 시킬 웹페이지 URL을 별도로 지정해줄 수도 있으니 참고하세요. 

 

BeautifulSoup 의 "html.parser"를 이용하여 HTML tag는 제외한 상태에서 HTML 콘텐츠의 문자열로부터 텍스트를 추출할 수 있습니다. 

 

아래 예제에서는 "https://www.langchain.com/" 페이지와 max_depth=2 까지의 하위 child pages 페이지들로부터 텍스트를 추출하였습니다. 

 

from langchain.document_loaders.recursive_url_loader import RecursiveUrlLoader

from bs4 import BeautifulSoup as Soup

url = "https://www.langchain.com/"

loader = RecursiveUrlLoader(
    url=url, 
    # the maximum depth to crawl, by default 2. 
    # If you need to crawl the whole website, set it to a number that is large enough would simply do the job.
    max_depth=2, 
    # extract plain text from a string of HTML content, without any HTML tags.
    extractor=lambda x: Soup(x, "html.parser").text
)

docs = loader.load()

 

 

LangChain main web page

 

 

LangChain의 메인 웹 페이지에서 총 7개 페이지로부터 텍스트를 추출해서 로드했습니다. 

 

len(docs)
# 7

 

 

모든 child pages 의 웹페이지를 순회를 하면서 텍스트를 추출해서 로드할 때 (a) page content, (b) metadata 를 가져옵니다. 그리고 각각에 대해서 접근해서 확인해 볼 수 있습니다. 

 

 

(a) Page Content

 

docs[0].page_content[:200]

# '\n\n\n\n\nLangChain\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nð\x9f¦\x9cð\x9f\x94\x97 
# LangChainLangSmithLangServeAgentsRetrievalEvaluationBlogDocsPartnersð\x9f¦\x9cð\x9f\x94
# \x97 LangChainð\x9f¦\x9cð\x9f\x94\x97 
# LangChain
# Catch up on the latest in LangSmith on YouTube.Bui'

 

 

(b) Metadata

 

docs[0].metadata

# {'source': 'https://www.langchain.com/',
#  'title': 'LangChain',
#  'description': 'LangChainâ\x80\x99s flexible abstractions and extensive toolkit unlocks developers to build context-aware, reasoning LLM applications.',
#  'language': 'en'}



docs[1].metadata

# {'source': 'https://www.langchain.com/langserve',
#  'title': 'LangServe',
#  'description': 'Deploy your LLM application with confidence. LangServe lets you deploy LangChain runnables and chains as a REST API.',
#  'language': 'en'}



docs[2].metadata

# {'source': 'https://www.langchain.com/use-case/agents',
#  'title': 'LangChain Agents',
#  'description': 'Turn your LLMs into reasoning engines.',
#  'language': 'en'}


docs[3].metadata

# {'source': 'https://www.langchain.com/use-case/retrieval',
#  'title': 'Retrieval',
#  'description': 'Personalize and Contextualize your LLM Application.',
#  'language': 'en'}

 

 

 

[ Reference ]

* LangChain - Recursive URL: 
https://python.langchain.com/docs/integrations/document_loaders/recursive_url

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!

 

728x90
반응형
Posted by Rfriend
,

OpenAI의 프레임워크에서 "대규모 언어 모델(Large Language Model, LLM)"과 "챗 모델(Chat Model)"이라는 용어는 언어 모델의 두 가지 다른 측면이나 구현을 나타냅니다.  


1. LLM (Large Language Model) 

- 일반 설명: LLM은 OpenAI가 개발한 GPT-3나 GPT-4와 같은 대규모 언어 모델을 가리키는 광범위한 용어입니다. 이 모델들은 인간과 유사한 텍스트를 이해하고 생성할 수 있게 해주는 많은 수의 파라미터로 특징지어집니다. 

- 기능: LLM은 다양한 인터넷 텍스트로 훈련됩니다. 따라서 번역, 요약, 질문 답변 등 다양한 언어 작업을 수행할 수 있습니다. 

- 유연성: 이 모델들은 대화 작업을 위해 특별히 조정되지 않았습니다. 대신, 챗봇을 포함하여 다양한 애플리케이션에 적용될 수 있는 범용 모델입니다. 또한 콘텐츠 생성, 코딩 지원 등 다른 분야에도 확장됩니다. 

- 훈련 데이터: 이 모델들의 훈련 데이터는 인터넷에서 다양한 텍스트 소스를 포함하며, 언어, 맥락, 지식에 대한 광범위한 이해를 제공합니다. 


2. Chat Model

- 특징: 현재 여러분이 상호작용하고 있는 챗 모델처럼, 챗 모델은 대화 작업에 특화된 대규모 언어 모델의 더 전문화된 구현입니다. 

- 기능: 대화 참여, 질문 답변, 작업 지원, 그리고 여러 차례에 걸쳐 일관되고 맥락을 인식하는 대화를 유지(maintain a coherent and context-aware conversation over multiple turns)하는 데에 설계되었습니다.

- 훈련 및 조정: 일반 대규모 언어 모델을 기반으로 하지만, 대화적인 맥락에서 뛰어난 성능을 발휘하기 위해 추가적인 훈련이나 조정을 거칩니다. 이는 대화 데이터셋에 대한 훈련이나 대화에서의 성능을 개선하기 위한 강화 학습 기법을 사용할 수 있습니다. 

- 사용자 경험: 사용자 친화적이고 매력적인 대화 경험을 제공하는 데 중점을 둡니다. 종종 적절하고 맥락에 맞는 응답을 보장하기 위한 추가적인 안전장치나 지침이 포함됩니다. 


요약하자면, LLM은 다양한 언어 작업을 수행할 수 있는 범용 대규모 언어 모델인 반면, Chat Model은 대화 상호작용 및 대화에 최적화된 LLM의 특화된 버전(maintain a coherent and context-aware conversation over multiple turns)입니다. 

 

 

이번 포스팅에서는 LangChain으로 OpenAI의 ChatModel을 활용한 챗봇을 만들 때 인풋 메시지를 작성하는 방법을 소개하겠습니다. 

 

(1) SystemMessage(), HumanMessage()를 이용한 Messages 작성

(2) ChatPromptTemplate과 format_prompt()를 이용한 Formatted Prompts 작성

(3) ChatPromptTemplate 를 이용한 Formatted Prompts 작성

 

 

LangChain - OpenAI ChatModel - Messages

 

 

먼저 터미널에서 pip install 로 openai, langchain을 설치합니다. 

 

! pip install openai langchain

 

 

Prompt 작성과 Chat Model에 필요한 모듈을 importing 하고, 환경변수에 OpenAI API Key를 설정해줍니다. 

 

import os

from langchain.prompts import (
    ChatPromptTemplate, 
    SystemMessagePromptTemplate, 
    HumanMessagePromptTemplate,
)
from langchain.schema import SystemMessage, HumanMessage
from langchain.chat_models import ChatOpenAI

# set OpenAI API Key in your environment variables
os.environ["OPENAI_API_KEY"]="sk-xxxx..."

 

 

 

(1) SystemMessage(), HumanMessage()를 이용한 Messages 작성

 

Chat Model로는 OpenAI의 GPT-4 버전을 사용하겠습니다. 

 

# Chat Model
model = ChatOpenAI(temperature=0, model="gpt-4")

 

 

 

LangChain의 ChatModels 는 SystemMessage, HumanMessage, AIMessage의 3가지 종류의 메시지 클래스를 제공합니다. 

 

- (a) SystemMessage: 시스템 컴포넌트로부터의 메시지로, ChatModel에게 페르소나, 역할 설정.

- (b) HumanMessage: 사용자로부터의 메시지로, ChatModel에게 작업을 시키는 인풋 메시지.

- (c) AIMessage: AI 모델로부터의 메시지로, SystemMessage와 HumanMessage를 인풋으로 받아 아웃풋으로 반환하는 메시지 

 

 

아래 예에서는 langchain.schema 에서 SystemMessage, HumanMessage를 importing하여 리스트로 두 메시지를 묶어서 Chat Model 에 인풋으로 넣어주면 됩니다. 

 

Formatted message가 아닌 일반 메시지를 인풋으로 넣을 때 바로 사용할 수 있어서 편리합니다. 

 

# System, Human Messages
# Chat models accept List[BaseMessage] as inputs, 
# or objects which can be coerced to messages, 
# including str (converted to HumanMessage) and PromptValue.
messages = [
    SystemMessage(
        content="You are a helpful assisant that translate English to Korean."
    ), 
    HumanMessage(
        content="Translate this sentence from English to Korean: I love programming."
    ), 
]


model.invoke(messages)
# AIMessage(content='나는 프로그래밍을 사랑한다.')

 

 

 

(2) ChatPromptTemplate과 format_prompt()를 이용한 Formatted Prompts 작성

 

이번에는 Prompt 메시지의 내용을 기본 뼈대의 Template을 만들어놓고, Chat Model을 실행시킬 때 사용자가 맞춤형으로 Prompt의 일부 부분을 인풋으로 넣어주어 Formatted prompts 를 만들어 최종 Chat Model을 실행시킬 때 사용하는 ChatPromptTemplate을 소개하겠습니다. 

 

아래 Template 작성 예에서는 System template에서 {input_language}, {output_language} 부분, 그리고 Human template의 {text} 부분이 나중에 사용자가 최종 입력하는 값에 따라 입력이 되어서 최종 Formatted prompt 가 작성되는 방식입니다. 

 

먼저, System template, Human template을 만들고, 

 

# You can make use of templating by using a MessagePromptTemplate
# (a) System Message
system_template = """
You are a helpful assistant that translate {input_language} to {output_language}"""
system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)


# (b) Human Message
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

 

 

다음으로 ChatPromptTemplate.from_messages([템플릿 리스트...]) 를 사용하여 Chat Prompt를 만듭니다. 

 

# You can build a ChatPromptTemplate from one or more MessagePromptTemplates.
chat_prompt = ChatPromptTemplate.from_messages(
    [system_message_prompt, human_message_prompt]
)

 

 

마지막으로, chat_prompt.format_prompt() 에서 아래처럼 사용자 인풋에 해당하는 변수 별 값을 입력해주면 ChatPromptValue()를 반환합니다. 

 

# ChatPromptTemplate’s format_prompt – this returns a PromptValue, 
# which you can convert to a string or Message object, 
# depending on whether you want to use the formatted value as input to an llm or chat model.
chat_prompt.format_prompt(
    input_language="English", 
    output_language="Korean",
    text="I love programming.")

# ChatPromptValue(messages=[
# SystemMessage(content='\nYou are a helpful assistant that translate English to Korean'), 
# HumanMessage(content='I love programming.')])

 

 

chat_prompt.format_prompt().to_messages() 를 뒤에 붙여주면 [SystemMessage, HumanMessage]를 반환합니다. --> 다음 단계에서 ChatModel에 인풋으로 넣어줍니다. 

 

# to_messages()
chat_prompt.format_prompt(
    input_language="English", 
    output_language="Korean",
    text="I love programming.").to_messages()

# [SystemMessage(content='\nYou are a helpful assistant that translate English to Korean'),
#  HumanMessage(content='I love programming.')]

 

 

ChatModel 에 위에서 작성한 [SystemMessage, HumanMessage] 를 인풋으로 넣어 invoke()로 실행시킵니다. 

 

# Get a chat completion from the formatted messages
model.invoke(
    chat_prompt.format_prompt(
        input_language="English", 
        output_language="Korean",
        text="I love programming.").to_messages()
)

# AIMessage(content='나는 프로그래밍을 사랑한다.')

 

 

 

 

(3) ChatPromptTemplate 를 이용한 Formatted Prompts 작성

 

위의 (2)번 방법이 Formatted Prompts를 작성하는 방법이었는데요, 코드가 길고 좀 복잡한 면이 있습니다. 이번의 (3)번 방법은 보다 직관적이고 코드도 간결해서 사용하기에 상대적으로 편리합니다. 

 

ChatPromptTemplate.from_messages([("system", "Message..."), ("human", "Message...")]) 로 Chat Prompts 를 작성해줍니다. 

 

# Chat Prompts
final_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful assistant that translate {input_language} to {output_language}"), 
        ("human", "{text}")
    ]
)

 

 

'|'로 Prompt와 Chat Model을 Chaining 해주고, invoke({"인풋변수": "인풋 값"}) 형식으로 딕션어리에 키:값 쌍으로 입력해주면 됩니다. (2)번 방법보다 상대적으로 직관적이어서 이해하기 쉽습니다. 

 

# Chainning using '|' operator
chain = final_prompt | model


# Invoke a chain
chain.invoke({
    "input_language": "English", 
    "output_language": "Korean", 
    "text": "I love programming."}
)

# AIMessage(content='나는 프로그래밍을 사랑한다.')

 

 

[ Reference ]

- LangChain - ChatOpenAI: https://python.langchain.com/docs/integrations/chat/openai

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

LangChain은 Harrison Chase에 의해 만들어진 것으로, 대형 언어 모델을 사용하여 응용 프로그램을 보다 효율적이고 효과적으로 구축하기 위한 도구 세트입니다. LangChain에서 "OpenAI Callback"은 LangChain 프레임워크 내에서 GPT-3 또는 GPT-4와 같은 OpenAI의 언어 모델과 상호 작용하기 위해 사용되는 함수나 메커니즘을 말합니다. 

LangChain의 OpenAI 콜백은 OpenAI 언어 모델로 쿼리를 보내고 그로부터 응답을 받는 인터페이스 역할을 합니다. 이것은 자연 언어 이해와 생성 기능을 필요로 하는 응용 프로그램에 있어 중요합니다. 콜백을 사용함으로써 개발자들은 대화 생성, 텍스트 요약, 질문 답변 등 다양한 기능을 포함한 응용 프로그램에 GPT-3 또는 GPT-4의 강력한 언어 처리 기능을 통합할 수 있습니다. 

이 콜백은 OpenAI의 API와 상호 작용하는 세부 사항을 추상화함으로써 개발자들이 API 통신의 복잡성에 대해 걱정하지 않고 자신들의 응용 프로그램의 고급 로직에 집중할 수 있게 해줍니다. 

 

LangChain의 get_openai_callback() 메소드를 사용하면 OpenAI LLM 모델 API 서비스에 대한 토큰 사용을 트래킹할 수 있습니다. (2024년.1월 현재 OpenAI API에 대해서만 구현이 되어 있음). 애플리케이션을 운영하는 입장에서는 사용하는 토큰 양(total tokens)과 소요되는 비용(total costs)을 트래킹하면서 적정 예산 범위 내 가용 가능한지를 점검하고 관리할 필요가 있을 것입니다. 

 

 

LangChain - OpenAI Callback - Tracking token usage

 

 

먼저, 터미널에서 pip install 을 사용해서 openai, langchain 모듈을 설치합니다. 

 

! pip install openai langchain

 

 

 

다음으로, 예제 실습에 필요한 모듈을 importing 합니다. 

LangChain의 Callbacks 클래스에서 get_openai_callback() 메소드를 import 해서 OpenAI API 서비스에서 사용한 토큰에 대해서 트래킹을 해보겠습니다. 

OpenAI의 GPT-4 Chat 모델을 사용하겠으며, OpenAI API Key를 환경변수로 입력해주었습니다. 

 

import os

from langchain.callbacks import get_openai_callback
from langchain.chat_models import ChatOpenAI

os.environ["OPENAI_API_KEY"]="sk-xxxx..." # set with yours

# initiate ChatOpenAI LLM model
model = ChatOpenAI(model="gpt-4")

 

 

 

매우 간단하게 1개의 호출에 대한 API token 사용에 대한 트래킹과, 복수의 API 호출에 대한 토큰 사용 트래킹을 나누어서 소개하겠습니다. 

 

(1) get_openai_callback()을 이용해 1개의 호출에 대한 토큰 사용 트래킹

 

# Track token usage for specific calls.
# It is currently only implemented for the OpenAI API.
with get_openai_callback() as cb:
    result = model.invoke("Tell me a joke about birds")
    print("result:", result)
    print("---" * 10)
    print(cb)
    
# result: content="Why don't birds use Facebook?\n\nBecause they already tweet enough!"
# ------------------------------
# Tokens Used: 26
# 	Prompt Tokens: 13
# 	Completion Tokens: 13
# Successful Requests: 1
# Total Cost (USD): $0.00117

 

 

 

(2) get_openai_callback()을 이용해 여러개의 호출에 대한 토큰 사용 트래킹

 

복수개의 API 호출에 대한 토근 사용 트래킹도 가능합니다. 아래에 Callback 의 각 속성 정보에 접근하는 방법도 소개하였습니다. 가령, 총 토근 수 (cb.total_tokens), 프롬프트 토큰 수 (cb.prompt_tokens), 답변 생성 토큰 수 (cb.completion_tokens), 총 소요 비용 (cb.total_cost) 등의 속성 정보를 개별적으로 접근해서 확인할 수 있습니다. 

 

# Anything inside the context manager will get tracked.
# Here’s an example of using it to track multiple calls in sequence.
with get_openai_callback() as cb:
    result_1 = model.invoke("Tell me a joke about birds")
    result_2 = model.invoke("Tell me a joke about dogs")
    
    print(f"Result 1: {result_1}")
    print(f"Result 2: {result_2}")
    print("-----" * 10)
    print(cb)
    print("-----" * 10)
    print(f"Total tokens: {cb.total_tokens}")
    print("-----" * 10)
    print(f"Prompt Tokens: {cb.prompt_tokens}")
    print("-----" * 10)
    print(f"Completion Tokens: {cb.completion_tokens}")
    print("-----" * 10)
    print(f"Total Cost (USD): ${cb.total_cost}")

    
# Result 1: content="Why do seagulls fly over the sea?\n\nBecause if they flew over the bay, they'd be bagels!"
# Result 2: content='Why did the scarecrow adopt a dog?\n\nBecause he needed a "barking" buddy!'
# --------------------------------------------------
# Tokens Used: 69
# 	Prompt Tokens: 26
# 	Completion Tokens: 43
# Successful Requests: 2
# Total Cost (USD): $0.0033599999999999997
# --------------------------------------------------
# Total tokens: 69
# --------------------------------------------------
# Prompt Tokens: 26
# --------------------------------------------------
# Completion Tokens: 43
# --------------------------------------------------
# Total Cost (USD): $0.0033599999999999997

 

 

 

[ Reference ]

* LangChain - Tracking token usage: 
https://python.langchain.com/docs/modules/model_io/chat/token_usage_tracking

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 LangChain 으로 LLM 모델 기반 애플리케이션을 구축할 때 캐시(Cache)를 사용하여 응답 속도를 향상시키고 계산 부하를 줄이는 방법을 소개하겠습니다. 

 

1. 캐시(Cache)란 무엇인가?

2. LangChain 으로 LLM 기반 애플리케이션 구축 시 사용하는 캐시 유형

3. LangChain 에서 Cache 적용해보기

 

 

LangChain - Cache

 

 

1. 캐시(Cache)란 무엇인가? 

 

"캐시(Cache)"는 컴퓨팅에서 데이터를 저장하여 이후 데이터 요청을 더 빠르게 처리할 수 있도록 하는 하드웨어 또는 소프트웨어 구성 요소를 말합니다. 캐시에 저장되는 데이터는 이전 계산의 결과거나 다른 곳에 저장된 데이터의 복사본일 수 있습니다. 캐시는 주로 데이터 접근 속도를 높이고, 주 메모리나 더 먼 저장 위치에서 데이터를 가져오는 시간을 줄이는 데 사용됩니다. 

LangChain과 같은 언어 모델 애플리케이션 구축 프레임워크의 맥락에서, 캐싱은 성능과 효율성을 향상시키는 데 중요한 역할을 합니다. LangChain과 같은 애플리케이션에서 캐시가 수행하는 기능은 다음과 같습니다. 

(1) 응답 속도 향상: GPT-3 또는 GPT-4와 같은 언어 모델이 쿼리를 처리할 때, 복잡한 계산을 수행해야 할 수 있으며, 이는 상당한 시간이 소요될 수 있습니다. 비슷한 쿼리가 자주 발생하는 경우, 결과를 캐싱하면 이러한 반복 쿼리에 대한 응답 시간을 크게 단축할 수 있습니다. 

(2) 계산 부하 감소: 이전에 계산된 결과를 저장함으로써, 언어 모델이 동일한 계산을 여러 번 수행할 필요를 줄일 수 있습니다. 이는 비용이 들어가는 언어 모델에 대한 호출 API 호출을 줄이는 데 특히 유용할 수 있습니다. 

(3) 사용자 경험 개선: 높은 사용자 상호 작용을 가진 애플리케이션, 예를 들어 챗봇이나 대화형 도구에서 자주 요청되는 정보나 응답을 캐싱하면 사용자는 즉각적인 피드백을 받을 수 있어 전반적인 경험이 향상됩니다. 

(4) 맞춤화 및 컨텍스트 유지: 경우에 따라 캐싱은 세션 동안 컨텍스트나 사용자 특정 데이터를 유지하는 데 사용될 수 있어 언어 모델과의 보다 개인화되고 컨텍스트에 맞는 상호 작용을 가능하게 합니다. 


전반적으로, LangChain과 같은 애플리케이션에서 캐싱의 역할은 데이터 및 계산 결과를 지능적으로 저장하고 재사용함으로써 효율성, 속도 및 사용자 경험을 향상시키는 것입니다. 

 

 

2. LangChain 으로 LLM 기반 애플리케이션 구축 시 사용하는 캐시 유형


LangChain은 언어 모델 애플리케이션을 구축하기 위한 프레임워크인데, 이러한 맥락에서 일반적으로 사용되는 캐시 유형은 다음과 같습니다. 

- 메모리 캐시 (Memory Cache): 언어 모델 계산 또는 API 응답 결과를 임시로 저장하는 데 사용됩니다. 이러한 작업이 자원을 많이 사용할 수 있으므로, 메모리에 결과를 캐싱하면 반복 쿼리나 작업에 대한 성능을 크게 향상시킬 수 있습니다. 

- 디스크 캐시 (Disk Cache): 더 크거나 시간에 민감하지 않은 데이터에 대해 디스크 캐싱이 사용될 수 있습니다. 이는 대용량 데이터셋이나 언어 모델 출력을 저장하는 데 특히 유용하며, 메모리 캐시의 속도가 필요하지 않지만 자주 접근되어 캐싱의 이점을 볼 수 있는 경우에 해당합니다. 

- 애플리케이션 캐시 (Application Cache): LangChain은 애플리케이션 프레임워크이므로 애플리케이션 특정 캐싱 메커니즘을 사용합니다. 여기에는 사용자 세션, 진행 중인 대화의 컨텍스트 또는 특정 사용자 환경 설정 및 데이터를 캐싱하는 것이 포함됩니다. 

- 데이터베이스 캐시 (Database Cache): LangChain이 데이터 저장 및 검색을 위해 데이터베이스와 상호 작용하는 경우, 일반적인 쿼리나 데이터 검색 작업을 가속화하기 위해 데이터베이스 캐싱이 사용될 수 있습니다. 

- 웹 캐시 (Web Cache): LangChain 애플리케이션이 웹 소스에서 데이터를 가져올 때, 웹 캐싱 메커니즘을 사용하여 웹 콘텐츠를 저장하고 빠르게 검색할 수 있습니다. 

- CDN 캐시 (CDN Cache): 애플리케이션이 웹을 통해 넓은 지역의 사용자 기반에 콘텐츠를 제공하는 경우, 사용자 위치에 가까운 곳에서 정적 리소스를 캐시하기 위해 CDN(콘텐츠 전송 네트워크,Content Delivery Network) 캐시를 사용할 수 있습니다. 


LangChain 애플리케이션에서 캐시 유형의 선택은 애플리케이션의 특정 요구 사항, 예를 들어 데이터의 성격, 접근 빈도, 필요한 접근 속도, 자원 제한 등에 따라 달라집니다

 

 

 

3. LangChain 에서 Cache 적용해보기

 

터미널에서 pip install로 openai, langchain을 설치합니다. 

 

! pip install openai langchain

 

 

 

인메모리 캐시(InMemory Cache)와 SQLite 데이터베이스 캐시 (SQLite Database Cache)를 순서대로 소개하겠습니다. 

 

3-1. InMemory Cache

 

먼저 메모리 캐시 (Memory Cache)를 이용하는 방법을 소개합니다.  

LLM 모델은 OpenAI의 "gpt-3.5-turbo"를 이용하겠으며, 환경변수로 OPENAI_API_KEY를 등록해줍니다. 

 

# (1) InMemoryCache
from langchain.globals import set_llm_cache
from langchain.llms import OpenAI

import os

os.environ["OPENAI_API_KEY"]="sk-xxxx..." # set with yours

 

 

캐시의 효과를 더 강조하기 위해서 조금 느린 LLM 모델을 사용하겠습니다. 

 

# To make the caching really obvious, let's use a slower model.
model = OpenAI(model="gpt-3.5-turbo-instruct", n=3)

 

 

%%time 으로 실행 시간을 측정해보겠습니다. 처음 사용할 때는 캐시가 적용되지 않기 때문에 시간이 상대적으로 오래 걸립니다. 

 

%%time
from langchain.cache import InMemoryCache

set_llm_cache(InMemoryCache())

# The first time, it is not yet in cache, so it should take longer
model.invoke("Tell me a joke about birds.")

# CPU times: total: 688 ms
# Wall time: 1.35 s
# '\n\nWhy did the chicken go to the seance?\n\nTo talk to the other side of the road!'

 

 

똑같은 사용자 쿼리를 LLM 모델에 보내면 캐시에 저장되어 있는 응답을 사용하여 즉시 응답해주기 때문에 소요시간이 대폭 줄어들었음을 알 수 있습니다. 

 

%%time
# The second time it is, so it goes faster
model.invoke("Tell me a joke about birds.")

# CPU times: total: 15 ns
# Wall time: 12 ns
# '\n\nWhy did the chicken go to the seance?\n\nTo talk to the other side of the road!'

 

 

 

3-2. SQLite Database Cache

 

다음으로 SQLLite Database Cache를 사용하기 위해 database path를 설정해줍니다. 

 

## (2) SQLite Cache
# We can do the same thing with a SQLite Cache
from langchain.globals import set_llm_cache
from langchain.cache import SQLiteCache

set_llm_cache(SQLiteCache(database_path=".langchain.db"))

# To make the caching really obvious, let's use a slower model.
model = OpenAI(model="gpt-3.5-turbo-instruct", n=3)

 

 

첫번째 사용자 쿼리 실행 시간을 측정해보겠습니다. 

 

%%time
# The first time, it is not yet in cache, so it should take longer.
model.invoke("Tell me a joke about birds")

# CPU times: total: 46.9 ms
# Wall time: 764 ms
# '\n\nWhy did the chicken go to the seance?\n\nTo talk to his ghost hen-cestors!'

 

 

위와 똑같은 사용자 쿼리를 LLM 모델에 보내보면, 이번에는 SQLite Cache에 저장되어 있던 답변이 신속하게 반환됩니다. 

 

%%time
# The second time it is, so it goes faster
model.invoke("Tell me a joke about birds")

# CPU times: total: 15.6 ms
# Wall time: 7.56 ms
# '\n\nWhy did the chicken go to the seance?\n\nTo talk to his ghost hen-cestors!'

 

 

[ Reference ]

- LangChain Cache: https://python.langchain.com/docs/modules/model_io/llms/llm_caching

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 LangChain 에서 LLM 체인을 호출할 때 사용하는 인터페이스에 대해서 소개하겠습니다. LangChain의 인터페이스는 크게 아래처럼 2가지로 구분할 수 있습니다. 

 

(1) Standard Interface: invoke(), stream(), batch()

(2) Async Interface: ainvoke(), astream(), abatch(), astream_log()

 

LangChain - Interface

 

(1) Standard Interface: invoke(), stream(), batch()

 

표준 인터페이스에는 invoke(), stream(), batch()가 있습니다. 표준 인터페이스는 LLM 모델 체인을 정의하고 표준 방식으로 호출(call a chain in a standard way)하는 것을 쉽게 만들어줍니다. 

 

먼저, 터미널에서 pip install을 사용해서 openai, langchain 모듈을 설치해줍니다. 

 

! pip install openai langchain

 

 

필요한 모듈을 importing하고, OpenAI의 LLM 모델을 API로 사용하기 위해 OPENAI_API_KEY를 환경변수로 등록해줍니다. 

 

import os

from langchain.llms import OpenAI
from langchain_core.output_parsers import StrOutputParser

os.environ["OPENAI_API_KEY"]="sk-xxxx...." # set with yours

llm = OpenAI()
parser = StrOutputParser()

chain = llm | parser

 

 

각 인터페이스에 대해 하나씩 예를 들어보겠습니다. 굳이 추가적인 설명이 필요치 않아서 제목과 예시를 보면 되겠습니다. 

 

 

(1-1) invoke(): 입력에 대해 체인을 호출함

 

# invoke(): call the chain on an input

chain.invoke("What are some colors of rainbow?")
# The colors of the rainbow are red, orange, yellow, green, blue, indigo, and violet.

 

 

 

(1-2) stream(): 응답의 청크(chunk)를 스트리밍함

 

# stream(): stream back chunks of the response

for chunk in chain.stream(
    "What are some colors of rainbow? Only answer the colors one by one per a line."):
    print(chunk, end="",  flush=True)
    
# Red
# Orange
# Yellow
# Green
# Blue
# Indigo
# Violet

 

참고로, flush=True를 설정하면 응답 텍스트가 출력되자마자 버퍼를 비워서 즉시 화면에 출력됩니다. 설정하지 않으면 파이썬은 자체적으로 적절한 시점에서 버퍼를 비울 때까지 데이터를 버퍼에 저장합니다. 이를 통해 프로그램이 실행되는 동안 실시간으로 데이터를 출력하거나 로깅을 할 때 유용하게 사용할 수 있습니다. 

 

 

 

(1-3) batch(): 입력 리스트 (a list of inputs)에 대해 체인을 호출함

 

# batch(): call the chain on a list of inputs
chain.batch([
    "What are some colors of rainbow?", 
    "Where is the capital city of South Korea?", 
    "When did the World War II happen?"
])

# ['\n\nThe colors of the rainbow are red, orange, yellow, green, blue, indigo, and violet.', 
# '\n\nThe capital city of South Korea is Seoul.', '
# \n\nWorld War II began on September 1, 1939, when Nazi Germany invaded Poland.']

 

 

 

(2) Async Interface: ainvoke(), astream(), abatch(), astream_log()

 

"async" 키워드는 비동기 프로그래밍(Asynchronous programming)을 말합니다. 이 함수는 비동기 함수로 정의되었으며, 이 함수 안에서 "await" 키워드를 사용하여 다른 비동기 함수가 완료될 때까지 기다릴 수 있습니다. 이를 통해 동시성 및 병렬성을 활용하여 더 효율적인 프로그램을 작성할 수 있습니다. 

예를 들어, 네트워크 요청이나 파일 입출력과 같은 I/O 작업을 수행할 때 비동기 프로그래밍은 프로그램이 블록되지 않고 다른 작업을 수행할 수 있게 해줍니다. 

Python 3.5부터 "async"와 "await" 키워드가 도입되어 비동기 프로그래밍을 지원하며, 이를 통해 asyncio 모듈과 같은 도구를 사용하여 비동기 코드를 작성할 수 있습니다. 

 

 

(2-1) ainvoke(): 입력에 대해 체인을 비동기적으로 호출함

 

# ainvoke(): call the chain on an input async

await chain.ainvoke("What are some colors of rainbow?")
# '\n\nThe colors of the rainbow are red, orange, yellow, green, blue, indigo, and violet.'

 

 

 

(2-2) astream(): 응답의 청크를 비동기적으로 스트리밍함

 

비동기적으로 처리하고 바로 바로 스트리밍 하기 때문에 중간에 공백 라인이 생겼습니다. 

 

# astream(): stream back chunks of the response async

async for chunk in chain.astream(
    "What are some colors of rainbow? Only answer the colors one by one per a line."):
    print(chunk)
#
#
# Red
#
#
# Orange
#
#
# Yellow
#
#
# Green
#
# Blue
# Indigo
# Violet

 

 

 

(2-3) abatch(): 입력 리스트(a list of inputs)에 대해 체인을 비동기적으로 호출함

 

# abatch(): call the chain on a list of inputs async
await chain.abatch([
    "What are some colors of rainbow?", 
    "Where is the capital city of South Korea?", 
    "When did the World War II happen?"
])

# ['\n\n-Red\n-Orange\n-Yellow\n-Green\n-Blue\n-Indigo\n-Violet',
#  '\n\nThe capital city of South Korea is Seoul.',
#  '\n\nWorld War II began on September 1, 1939, when Nazi Germany invaded Poland.']

 

 

 

(2-4) astream_log(): 최종 응답뿐만 아니라 중간 단계 로그를 그때 그때 스트리밍함

 

중간 단계의 세부 로그 데이터를 확인할 수 있습니다. 

 

# astream_log(): stream back intermediate steps as they happen, in addition to the final response
async for chunk in chain.astream_log(
    "What are some colors of rainbow? Only answer the colors one by one per a line."):
    print("---" * 20)
    print(chunk)
    
# ------------------------------------------------------------
# RunLogPatch({'op': 'replace',
#   'path': '',
#   'value': {'final_output': None,
#             'id': '98250f70-4772-4afe-9e3e-e815b1c0ca78',
#             'logs': {},
#             'streamed_output': []}})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/OpenAI',
#   'value': {'end_time': None,
#             'final_output': None,
#             'id': '7b2ca22e-71e0-4a03-9775-801f6c92f3d6',
#             'metadata': {},
#             'name': 'OpenAI',
#             'start_time': '2024-01-06T12:08:35.661',
#             'streamed_output_str': [],
#             'tags': ['seq:step:1'],
#             'type': 'llm'}})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/StrOutputParser',
#   'value': {'end_time': None,
#             'final_output': None,
#             'id': 'abebbbcd-7c1c-4af3-8a47-54c67df34bb8',
#             'metadata': {},
#             'name': 'StrOutputParser',
#             'start_time': '2024-01-06T12:08:36.061',
#             'streamed_output_str': [],
#             'tags': ['seq:step:2'],
#             'type': 'parser'}})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '\n'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '\n'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'Red'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\nRed'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': 'Red'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '\n'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\nRed\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'Orange'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\nRed\nOrange'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': 'Orange'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '\n'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\nRed\nOrange\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'Yellow'},
#  {'op': 'replace', 'path': '/final_output', 'value': '\n\nRed\nOrange\nYellow'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': 'Yellow'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '\n'},
#  {'op': 'replace',
#   'path': '/final_output',
#   'value': '\n\nRed\nOrange\nYellow\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/logs/OpenAI/streamed_output_str/-', 'value': '\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'Green\n'},
#  {'op': 'replace',
#   'path': '/final_output',
#   'value': '\n\nRed\nOrange\nYellow\nGreen\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/OpenAI/streamed_output_str/-',
#   'value': 'Green\n'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'Blue\nIndigo\nViolet'},
#  {'op': 'replace',
#   'path': '/final_output',
#   'value': '\n\nRed\nOrange\nYellow\nGreen\nBlue\nIndigo\nViolet'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/OpenAI/streamed_output_str/-',
#   'value': 'Blue\nIndigo\nViolet'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/OpenAI/final_output',
#   'value': {'generations': [[{'generation_info': {'finish_reason': 'stop',
#                                                   'logprobs': None},
#                               'text': '\n'
#                                       '\n'
#                                       'Red\n'
#                                       'Orange\n'
#                                       'Yellow\n'
#                                       'Green\n'
#                                       'Blue\n'
#                                       'Indigo\n'
#                                       'Violet',
#                               'type': 'Generation'}]],
#             'llm_output': None,
#             'run': None}},
#  {'op': 'add',
#   'path': '/logs/OpenAI/end_time',
#   'value': '2024-01-06T12:08:36.322'})
# ------------------------------------------------------------
# RunLogPatch({'op': 'add',
#   'path': '/logs/StrOutputParser/final_output',
#   'value': {'output': '\n\nRed\nOrange\nYellow\nGreen\nBlue\nIndigo\nViolet'}},
#  {'op': 'add',
#   'path': '/logs/StrOutputParser/end_time',
#   'value': '2024-01-06T12:08:36.322'})

 

 

[ Reference ]

- LangChain Interface: https://python.langchain.com/docs/expression_language/interface#async-invoke

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

Chain-of-Thought Prompting은 복잡한 작업에서 언어 모델의 성능을 향상시키기 위해 사용되는 기술입니다. Chain-of-Thought Prompting은 언어 모델이 답을 찾아가는 과정에서 "생각을 말로 표현하거나(think aloud)" 단계별 추론 과정(step-by-step reasoning)을 따르도록 유도하는 방식으로 구성됩니다. 이 접근법은 수학 문제(math word problems), 복잡한 이해 문제(complex comprehension questions) 또는 주어진 데이터로부터 추론을 요구하는 문제(tasks that require drawing inferences from given data)와 같이 여러 단계나 추론의 레이어가 필요한 문제에 특히 유용합니다. 

Chain-of-Thought Prompting 에서는 문제를 제기하는 것뿐만 아니라 문제를 해결하기 위해 취할 수 있는 일련의 논리적 단계(a sequence of logical stemps)를 포함하여 프롬프트를 구성합니다. 이 방법을 통해 모델은 유사한 추론 라인을 따르도록 유도되며, 이는 정확하고 잘 설명된 답변에 도달할 가능성을 높입니다. 예를 들어, 수학 문제의 해답을 간단히 묻는 대신에, 문제를 접근하는 방법의 분해를 프롬프트에 포함시켜 모델이 이러한 사고 과정을 반영하도록 유도할 수 있습니다. 

 

아래에는 Standard Prompting와 Chain-of-Thought Prompting를 비교해서 예시를 들어놓은 것입니다. (논문에 제시된 예시)  우측 예시가 Chain-of-Thought Prompting 예시인데요, 파란색으로 색깔 칠해진 부분이 수학 문제를 푸는데 사용한 로직을 차근 차근 단계별로 풀어서 예시를 제시해주었더니, LLM 모델이 Output을 생성할 때 역시 차근 차근 단계를 밟아서 문제 풀이 로직을 설명해가면서 정확하게 답을 생성했습니다. 

 

 

[ Standard Prompting vs. Chain-of-Thought Prompting 예시 ]

Standard Prompting vs. Chain-of-Thought Prompting

* source: Jason Wei, et.al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models", arXive:2201.11903v6, 2023

 

 

아래의 예시는 논문에 제시된 수학 (arithmetic), 상식 (common sense), 상징 추론 (symbolic reasoning) 영역의 Chain-of-Thought Prompting 예시입니다. (색깔 친 부분을 주시해서 보세요) 

 

 

[ Chain-of-Thought Prompting 예시 ]

Examples of Chain-of-Thought Prompting

* source: Jason Wei, et.al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models", arXive:2201.11903v6, 2023

 

 

LLM 모델로는 ChatGPT-3.5와 ChatGPT-4 를 사용하여 Standard Prompting과 위에서 소개한 Chain-of-Thought Prompting 을 번갈아서 적용해서 수학 계산이 단계적으로 필요한 논리적인 문제를 제시해보겠습니다. 

 

1. Standard Prompting using ChatGPT-3.5

 

원하는 답변 대신에 "상식적으로 통근 시간이 말이 안되게 오래 걸린다"는 답변을 했네요. ㅋㅋ 

(동문서답?) 

 

[ User ] :
Which is a faster way to get to work?
Option 1: Take a 1000 minute bus, then a half hour train, and finally a 10 minute bike ride.
Option 2: Take an 800 minute bus, then an hour train, and finally a 30 minute bike ride. 


[ ChatGPT-3.5 ] :
Neither option seems practical for a daily commute to work, as both involve extremely long travel times. In a typical workday, commuting for over 10-16 hours (800-1000 minutes) would not be feasible for most people. You should explore alternative transportation options that would significantly reduce your commute time, such as finding a closer place to live or considering other modes of transportation like carpooling, using a car, or telecommuting if possible. A daily commute of 800-1000 minutes is not realistic for most people due to the excessive time spent on transportation. 

 

 

 

2. Chain-of-Thought using ChatGPT-3.5

 

이번에는 Chain-of-Thought Prompting을 사용해서 비슷한 문제의 단계적인 풀이 과정을 제시해주고, 본 문제를 냈을 때, 원하는 Output을 역시 단계적으로 차근 차근 풀어서 정확한 답을 생성했네요. ChatGPT 나 사람이나 비슷한거 같아요. 사람도 이런 문제 풀 때 보면 차근 차근 단계를 밟아가면서 풀잖아요. 

하여튼, 대규모 언어모델 (Large Language Model, LLM)이 거대해지면서 어느 순간 창발(emergence)이 일어나서 다방면의 과제를 잘 수행하는 면을 보이는데요, Chain-of-Thought Prompting 처럼 프롬프트에 단계적 사고 논리의 예시를 주는 것만으로도 LLM 모델이 이를 금방 이해해서 문제의 답을 생성하는것을 보면 참 놀랍습니다! 

 

[ User ] :
Which is a faster way to get home?

Option 1: Take an 10 minutes bus, then an 40 minute bus, and finally a 10 minute train.
Option 2: Take a 90 minutes train, then a 45 minute bike ride, and finally a 10 minute bus.
Option 1 will take 10+40+10 = 60 minutes.
Option 2 will take 90+45+10=145 minutes.
Since Option 1 takes 60 minutes and Option 2 takes 145 minutes, Option 1 is faster.

Which is a faster way to get to work?
Option 1: Take a 1000 minute bus, then a half hour train, and finally a 10 minute bike ride.
Option 2: Take an 800 minute bus, then an hour train, and finally a 30 minute bike ride. 


[ ChatGPT-3.5 ] :
Option 2 is the faster way to get to work.

Option 1:

1000 minutes bus
Half-hour (30 minutes) train ride
10-minute bike ride
Total time for Option 1: 1000 + 30 + 10 = 1040 minutes


Option 2:

800 minutes bus
1-hour (60 minutes) train ride
30-minute bike ride
Total time for Option 2: 800 + 60 + 30 = 890 minutes

Option 2 takes 890 minutes, while Option 1 takes 1040 minutes, making Option 2 the faster choice for getting to work. However, it's important to note that both options have extremely long commute times, and such long commutes are highly impractical in real-life situations. 

 

 

 

3. Standard Prompting using ChatGPT-4

 

ChatGPT-4 는 비록 Standard Prompting 을 사용해도 이 문제가 여러 단계를 차근 차근 거쳐서 문제를 논리적으로 풀어야 하는지를 알고 Python 을 실행시켜서 아래처럼 Python code를 짜서(!!) 문제를 풀고, 그 풀이 과정도 친절하게 설명해주고 있습니다. OpenAI 일 참 잘해요! 짝짝짝!! 추앙합니당~! 

(즉, ChatGPT-4에서는 Chain-of-Thought Prompting을 안해도 ChatGPT-4가 알아서 다 해줍니다!) 

 

[ User ]
Which is a faster way to get to work? 
Option 1: Take a 1000 minute bus, then a half hour train, and finally a 10 minute bike ride. 
Option 2: Take an 800 minute bus, then an hour train, and finally a 30 minute bike ride.

[ ChatGPT-4 ]

To determine which option is faster to get to work, we need to add up the total travel times for each option.

Option 1:

1000 minutes for the bus
30 minutes for the train
10 minutes for the bike ride

Option 2:
800 minutes for the bus
60 minutes (1 hour) for the train
30 minutes for the bike ride

Let's calculate the total travel time for each option.

The total travel time for Option 1 is 1040 minutes, and for Option 2, it's 890 minutes. Therefore, Option 2 is the faster way to get to work. ​

 

 

아래처럼 Python 이 자동 실행되어서 Python codes를 짜서 문제를 풀고, 이를 다시 자연어로 풀이과정과 답을 생성해줘요. 

 

## Python Codes
# Calculating the total travel time for each option

# Option 1
bus1 = 1000 # minutes
train1 = 30 # minutes
bike1 = 10 # minutes
total1 = bus1 + train1 + bike1

# Option 2
bus2 = 800 # minutes
train2 = 60 # minutes (1 hour)
bike2 = 30 # minutes
total2 = bus2 + train2 + bike2

total1, total2

Result
(1040, 890)

 

 

[ Reference ]

- source: Jason Wei, et.al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models", arXive:2201.11903v6, 2023

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. :-)

 

 

728x90
반응형
Posted by Rfriend
,

LangChain으로 LLM 모델을 사용한 애플리케이션을 만들 때 기존의 여러개 프롬프트를 재사용하고 조합해서 최종 프롬프트를 만들어서 사용해야 할 경우가 있습니다. 이때 LangChain의 PipelinePrompt 를 사용하면 되는데요, PipeliePrompt는 다음과 같이 Final promt, Pipeline prompts 의 두 개 부분으로 구성되어 있습니다. 

 

- (1) Final prompt: 최종적으로 반환되는 프롬프트. 

- (2) Pipeline prompts: string 이름과 prompt template 으로 구성되는 튜플의 리스트(a list of tuples). 각 프롬프트는 포맷이 정의되고, 동일한 이름의 변수로 파이프라인의 다음 template으로 전달됨. 

 

 

간단한 예를 들어보겠습니다. 

 

- (1) Final prompt: final_prompt

- (2) Pipeline prompts: introduction_prompt, example_prompt, start_prompt

- (3) Composing all together

       : PipelinePromptTemplate(

             final_prompt=final_prompt,

             pipeline_prompts=[

                 ("introduction", introduction_prompt),

                 ("example", example_prompt),

                 ("start", start_prompt)

             ])

 

 

LangChain - Pipeline Prompts

 

 

먼저, 터미널에서 pip install로 langchain, openai 모듈을 설치합니다. 

 

! pip install langchain openai

 

 

다음으로 실습에 필요한 모듈을 importing 합니다. 

from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

 

 

OpenAI의 Chat Model을 사용해서 답변을 생성할 것이므로 OPENAI API KEY를 환경변수에 등록합니다. 

 

import os 

os.environ["OPENAI_API_KEY"]="sk-xxxx..."

 

 

 

(1) Final prompt 생성

 

여러개의 하위 프롬프트를 구성요소로 가지는 최종 프롬프트를 작성합니다. (나중에 변수이름과 하위 구성요소 프롬프트를 튜플로 매핑시키고, 하나의 리스트로 묶어줍니다.)

 

# Final template and prompt
final_template = """{introduction}

{example}

{start}"""

final_prompt = ChatPromptTemplate.from_template(final_template)

 

 

 

(2) Pipeline prompts 생성

 

(2-1) instruction_prompt 생성

 

# Introduction template and prompt
introduction_template = """You are an expert on {topic}."""

introduction_prompt = ChatPromptTemplate.from_template(introduction_template)

 

 

(2-2) exmaple_prompt 생성

 

# Example template and prompt
example_template = """Here is an example of an interaction:

Input: {example_input}
Output: {example_output}"""

example_prompt = ChatPromptTemplate.from_template(example_template)

 

 

(2-3) start_prompt 생성

 

# Start template and prompt
start_template = """Now, do this for real. 

Input: {input}
Output:"""

start_prompt = ChatPromptTemplate.from_template(start_template)

 

 

 

(3) 여러개의 Pipeline prompts를 PipelinePrompt를 이용해서 하나의 Pipeline으로 조합하기    (Composing all together)  

 

# Composing multiple prompts together using PipelinePromptTemplate()
input_prompts = [
    ("introduction", introduction_prompt), 
    ("example", example_prompt), 
    ("start", start_prompt), 
]

pipeline_prompt = PipelinePromptTemplate(
    final_prompt=final_prompt, pipeline_prompts=input_prompts
)


pipeline_prompt.input_variables
# ['example_input', 'example_output', 'input', 'topic']

 

 

최종 pipeline_prompt를 구성하는 각 하위 프롬프트의 변수이름에 값을 입력해서 프린트해보았습니다. 이제 프롬프트는 준비가 다 되었습니다. 

 

print(
    pipeline_prompt.format(
        topic="Math", 
        example_input="It costs 2 dollar per apple. How much do you need in total to buy 10 apples?", 
        example_output="Since 2 x 10 = 20, you need a total of $20.", 
        input="How much does it cost to buy 6 cans of beer that cost $3 each?",
    )
)


# Human: [HumanMessage(content='You are an expert on Math.')]

# [HumanMessage(content='Here is an example of an interaction:\n\nInput: It costs 2 dollar per apple. How much do you need in total to buy 10 apples?\nOutput: Since 2 x 10 = 20, you need a total of $20.')]

# [HumanMessage(content='Now, do this for real. \n\nInput: How much does it cost to buy 6 cans of beer that cost $3 each?\nOutput:')]

 

 

 

(4) pipeline_prompt + Chat Model + Output Parser를 Chaining 하기

 

# Chat Model
model = ChatOpenAI(temperature=0, model='gpt-4')

# Output Parser
parser = StrOutputParser()

# Chaining
chain = pipeline_prompt | model | parser

 

 

 

(5) chain.invoke() 로 Chat Model 실행하고 결과 반환받기

 

두개를 실행시켜 봤는데 예제를 통해 가르켜준대로 잘 대답을 해주네요. 

 

chain.invoke({
    "topic": "Math", 
    "example_input": "It costs 2 dollar per apple. How much do you need in total to buy 10 apples?", 
    "example_output": "Since 2 x 10 = 20, you need a total of $20.", 
    "input": "How much does it cost to buy 6 cans of beer that cost $3 each?"
})

# 'Since 6 x 3 = 18, you need a total of $18.'



chain.invoke({
    "topic": "Math", 
    "example_input": "It costs 2 dollar per apple. How much do you need in total to buy 10 apples?", 
    "example_output": "Since 2 x 10 = 20, you need a total of $20.", 
    "input": "How much does it cost in total to buy 6 cans of beer that cost $3 each \
    and two snacks that cost $5 each?"
})

# 'Since (6 x $3) + (2 x $5) = $18 + $10 = $28, you need a total of $28.'

 

 

 

[ Reference ]

* LangChain - Pipeline: https://python.langchain.com/docs/modules/model_io/prompts/pipeline

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. :-)

 

728x90
반응형
Posted by Rfriend
,