2023년 10월 13일 금요일

트랜스포머 생성AI로 다국어 번역기, 문장 분류, 이미지 설명 텍스트 생성 프로그램 개발해보기

이 글은 트랜스포머(Transformer) 생성AI 모델로 다국어 번역기, 문장 분류, 이미지 설명 텍스트 생성 프로그램 개발 방법을 간략히 나눔한다. 쉽게 말해, 트랜스포머는 텍스트, 이미지 등 데이터를 숫자로 표현한 토큰으로 인코딩한 후, 목표 라벨 데이터 결과와 차이가 적은 방향으로 가중치인 어텐션(attension) 벡터를 갱신하여, 학습모델을 만드는 기술이다.

트랜스포머는 현재 문장 성격 및 특징 분류, 다국어 번역, 비전 이미지 설명 및 생성, 음성인식, Voice to Text, 음악작곡, 글 분류, 글 자동요약 등 다양한 영역에서 사용된다. 이 글은 관련 기능을 트랜스포머로 간단히 구현해 본다. 
트랜스포머 개념 및 아키텍처

트랜스포머의 이론적 개념 등은 아래 링크를 참고한다.

개발 준비
파이썬, CUDA 등이 설치되어 있다는 가정하에, 다음 명령어를 이용해, 관련 패키지를 설치한다.
pip install transfomers sentencepiece

간단한 텍스트 생성AI 개발
다음과 같이, 트랜스포머 모델에서 사전학습된 distilgpt2를 사용해, 간단한 텍스트 생성AI 코드를 개발해 본다.
from transformers import AutoTokenizer, AutoModelWithLMHead

tokenizer = AutoTokenizer.from_pretrained("distilgpt2")
model = AutoModelWithLMHead.from_pretrained("distilgpt2")

input_ids = tokenizer.encode("I like gpt because it's", return_tensors='pt')
greedy_output = model.generate(input_ids, max_length=12)

print(tokenizer.decode(greedy_output[0], skip_special_tokens=True))

실행, 결과 다음과 같이 I like gpt because it's 문장 이후를 자연스럽게 생성한다.

텍스트의 빈칸 단어 예측
다음은 MLM(Mask Language Model) 방식으로 학습된 모델을 이용해, [MASK] 토큰으로 정의된 단어를 문장 맥락을 고려해 예측하는 코드이다. 
from transformers import pipeline

unmasker = pipeline('fill-mask', model='albert-base-v2')
unmasker("mlm and nsp is the [MASK] task of bert.")

결과는 다음과 같다. 빈칸 단어가 잘 예측된 것을 확인할 수 있다.

이미지 설명 텍스트 생성
이제 주어진 이미지를 설명하는 텍스트를 생성해 본다. 목표는 입력 이미지에 대해 두마리의 고양이가 누워있음을 표현하는 텍스트를 얻는 것이다.

앞의 방식대로, 사전 훈련된 모델을 이용한다. 다음을 코딩한다.
import torch
import matplotlib.pyplot as plt
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import requests

url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

candidates = ["three cats lying on the couch", "a photo of a cat", "a photo of a dog", "a lion", "two cats lying on the cushion"]
inputs = processor(text=candidates, images=image, return_tensors="pt", padding=True)
plt.imshow(inputs['pixel_values'][0][0]);
processor.tokenizer.decode(inputs['input_ids'][0])

model.eval()  # eval mode
outputs = model(**inputs)

logits_per_image = outputs.logits_per_image
print(logits_per_image)

probs = logits_per_image.softmax(dim=1)
print(candidates[torch.argmax(probs).item()])

결과는 다음과 같이 이미지를 적절히 설명하고 있다. 

사전학습모델 파인튜닝하기 
기존 학습모델을 이용해, 텍스트 등을 생성하였지만, 그 결과가 마음에 들지 않는 다면, 파인튜닝을 해서 그 결과를 개선할 수 있다. 

다음은 튜닝하지 않았을 때 결과를 출력하는 코드이다. 
# no tuning
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch

tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=3)

dic = {0:'positive', 1:'neutral', 2:'negative'}
eval_list = ["I like apple", "I like pear", "I go to school", "I dislike mosquito", "I felt very sad", "I feel so good"]
ans = torch.tensor([0, 0, 1, 2, 2, 0])

model.eval()
with torch.no_grad():
  for article in eval_list:
    inputs = tokenizer.encode(article, return_tensors="pt",padding=True, truncation=True)
    outputs = model(inputs)

    logits = outputs.logits
    print(f"{dic[logits.argmax(-1).item()]}:{article}")

주어진 문장에 엉뚱한 답을 출력한다. 이는 원하는 결과가 아니다. 

각 문장을 원하는 결과를 얻도록, 다음의 파인튜닝 코드를 실행해 학습한다.
# fine turning
from transformers import AdamW

optimizer = AdamW(model.parameters(), lr=1e-5)
model.train()

epochs = 50
losses = []

for epoch in range(epochs):
    optimizer.zero_grad()
    inputs = tokenizer.batch_encode_plus(eval_list, return_tensors="pt", padding=True, truncation=True)
    outputs = model(**inputs, labels=ans)

    logits = outputs.logits
    loss = outputs.loss

    loss.backward()
    optimizer.step()

    losses.append(loss)
    print(f"epoch:{epoch+1}, loss:{loss}")

new_losses = [i.item() for i in losses]

결과를 다음과 같이 출력해 본다. 
# plot results
import matplotlib.pyplot as plt
plt.plot(new_losses)

dic = {0:'positive', 1:'neutral', 2:'negative'}
eval_list = ["I like apple", "I like pear", "I go to school", "I dislike mosquito", "I felt very sad", "I feel so good"]

model.eval()
preds = []
with torch.no_grad():
  for article in eval_list:
    inputs = tokenizer.encode(article, return_tensors="pt",padding=True, truncation=True)
    outputs = model(inputs)
    logits = outputs.logits
    pred = logits.argmax(-1).item()
    preds.append(logits.argmax(-1).item())
    print(f"{dic[pred]}:{article}")

각 문장이 어떤 느낌인지 원하는 결과로 예측한다. 

문장 요약하기
학습된 BERT 모델 중에 문장을 요약하는 기능을 하는 모델이 있다. 다음과 같이 코딩해 본다. 
import re
from transformers import BartTokenizer, TFBartForConditionalGeneration

model = TFBartForConditionalGeneration.from_pretrained('facebook/bart-large')
tokenizer = BartTokenizer.from_pretrained('facebook/bart-large')

article = """
A transformer is a deep learning architecture that relies on the parallel multi-head attention mechanism.[1] The modern transformer was proposed in the 2017 paper titled 'Attention Is All You Need' by Ashish Vaswani et al., Google Brain team. It is notable for requiring less training time than previous recurrent neural architectures, such as long short-term memory (LSTM),[2] and its later variation has been prevalently adopted for training large language models on large (language) datasets, such as the Wikipedia corpus and Common Crawl, by virtue of the parallelized processing of input sequence.[3] Input text is split into n-grams encoded as tokens and each token is converted into a vector via looking up from a word embedding table. At each layer, each token is then contextualized within the scope of the context window with other (unmasked) tokens via a parallel multi-head attention mechanism allowing the signal for key tokens to be amplified and less important tokens to be diminished. Though the transformer paper was published in 2017, the softmax-based attention mechanism was proposed earlier in 2014 by Bahdanau, Cho, and Bengio for machine translation,[4][5] and the Fast Weight Controller, similar to a transformer, was proposed in 1992 by Schmidhuber.[6][7][8]

This architecture is now used not only in natural language processing and computer vision,[9] but also in audio[10] and multi-modal processing. It has also led to the development of pre-trained systems, such as generative pre-trained transformers (GPTs)[11] and BERT[12] (Bidirectional Encoder Representations from Transformers).
"""
print(article)

article = re.sub(r"[:.]\[[0-9]+\](.*?)\([0-9]+\)|.?[([][0-9]+[])]|\n|\r", r"", article)
print(article)

inputs = tokenizer([article], max_length=1024, return_tensors='tf', truncation=True)
summary_ids = model.generate(inputs['input_ids'], num_beams=5, max_length=25)

print(''.join([tokenizer.decode(g, skip_special_tokens=True, clean_up_tokenization_spaces=False) for g in summary_ids]))

결과는 다음과 같다. 

참고로, 다음은 기본 BERT 모델보다 큰 문장을 처리하는 BigBERT 모델인 PEGASUS(Gap Sentence Generation)를 이용한 요약 문장 생성 코드이다. 이 모델은 앞서 MLM 문제 학습 모델과는 다르게, 문장 전체를 MASK해 다음 문장을 예측하도록 학습되었다. 
from transformers import PegasusForConditionalGeneration, PegasusTokenizer
import torch

model_name = 'google/pegasus-xsum'
device = 'cuda' if torch.cuda.is_available() else 'cpu'

tokenizer = PegasusTokenizer.from_pretrained(model_name)
model = PegasusForConditionalGeneration.from_pretrained(model_name).to(device)

inputs = [
          """
          Pretraining large neural language models, such as BERT, has led to impressive gains on many natural language processing (NLP) tasks. However, most pretraining efforts focus on general domain corpora, such as newswire and Web. A prevailing assumption is that even domain-specific pretraining can benefit by starting from general-domain language models. Recent work shows that for domains with abundant unlabeled text, such as biomedicine, pretraining language models from scratch results in substantial gains over continual pretraining of general-domain language models.
          """
]

batch = tokenizer(inputs, truncation=True, padding='longest', return_tensors="pt").to(device)
translated = model.generate(**batch)
generated_text = tokenizer.batch_decode(translated, skip_special_tokens=True)

print(generated_text[0])

다국어 번역하기(중국-영어-한글)
다음은 다국어 번역 학습 모델을 이용해, 글을 번역하는 코드이다. 
from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer

model = M2M100ForConditionalGeneration.from_pretrained("facebook/m2m100_418M")
tokenizer = M2M100Tokenizer.from_pretrained("facebook/m2m100_418M")

chinese_text = "我愛你. 你也愛我嗎"
tokenizer.src_lang = "zh"
encoded_zh = tokenizer(chinese_text, return_tensors="pt")
generated_tokens = model.generate(**encoded_zh, forced_bos_token_id=tokenizer.get_lang_id("en"))
print(tokenizer.batch_decode(generated_tokens, skip_special_tokens=True))

tokenizer.src_lang = "ko"
korean_text = "딥러닝 모델을 이용해, 사람들은 업무 생산성을 높이고 있지만, 어떤 사람들은 일자리에 큰 위협을 느끼고 있다."
encoded_ko = tokenizer(korean_text, return_tensors="pt")
generated_tokens = model.generate(**encoded_ko, forced_bos_token_id=tokenizer.get_lang_id("en"))
print(tokenizer.batch_decode(generated_tokens, skip_special_tokens=True))

english_text = "I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin, but by the content of their character."
tokenizer.src_lang = "en"
encoded_en = tokenizer(english_text, return_tensors="pt")
generated_tokens = model.generate(**encoded_en, forced_bos_token_id=tokenizer.get_lang_id("ko"))
print(tokenizer.batch_decode(generated_tokens, skip_special_tokens=True))

결과는 다음과 같다.

대화형 챗봇처럼 문장 생성하기
다음은 마이크로소프트에서 학습한 대규모 대화 텍스트 모델이다. 이를 이용해, 챗봇과 같은 자연스러운 문장을 생성할 수 있다.
from transformers import AutoTokenizer, AutoModelWithLMHead

tokenizer = AutoTokenizer.from_pretrained("microsoft/DialoGPT-small")
model = AutoModelWithLMHead.from_pretrained("microsoft/DialoGPT-small")

input_ids = tokenizer.encode("I like gpt because it's", return_tensors='pt')
greedy_output = model.generate(input_ids, max_length=30)

print(tokenizer.decode(greedy_output[0], skip_special_tokens=True))

마무리
트랜스포머를 이용하면, GPT와 같은 생성AI를 개발할 수 있다. 앞서 코드를 보면 알겠지만, 트랜스포머 사용 절차는 대부분 다음과 같다. 
1. 원하는 기능의 사전학습모델 다운로드
2. 해당 토크나이저 다운로드
3. 입력 데이터 토큰화
4. 모델에 토큰화된 데이터 입력해 출력 데이터 생성
5. 출력 데이터를 디코딩해 원하는 생성 데이터 획득

트랜스포머는 현재 문장 성격 및 특징 분류, 다국어 번역, 비전 이미지 설명 및 생성, 음성인식, Voice to Text, 음악작곡, 글 분류, 글 자동요약 등 다양한 영역에서 사용된다.


레퍼런스

2023년 10월 5일 목요일

Langchain 기반 개인화된 ChatGPT 대화, 코딩, QA 챗봇 자체 서비스 개발 방법

이 글은 개인화된 ChatGPT와 같은 생성AI 서비스 개발을 지원하는 Langchain (랭체인) 사용법을 나눔한다. 랭체인을 통해, LLM 모델을 사용한 자체 에이전트 서비스를 개발할 수 있다. 이 글은 ChatGPT, LLAMA2과 같은 LLM모델을 이용해, RAG(검색증강생성. Retrieval-Augmented Generation)을 통해 전문적으로 개인화된 결과를 생성하는 개발과정을 포함한다.  
랭체인 개념

LangChain은 LLM 애플리케이션을 구축하기 위한 오픈 소스 개발자 프레임워크이다. 이 글은LangChain 사용사례, 예를 들어, 본인의 데이터를 이용해 AI 에이전트를 개발하는 방법에 중점을 둘 것이다.
개요
LangChain은 Andrew Ng 교수, Harrison Chase에 의해 개발되었다. 랭체인은 다음 기능을 지원한다. 
  • 문서 로딩
  • 문서 분할
  • RAG, 벡터 스토어임베딩 (참고. Usage Guide | Chroma)
  • 100개 이상의 문서 로더 (예. PDF, JSON, HTML, FILE 등), 문서 이미지 추출
  • 검색
  • 질의응답
  • 메모리 기록
  • LangServe 기반 배포 지원
LangChain 모듈 아키텍처
LangChain RAG 개념

랭체인은 다음과 같은 다양한 문서를 데이터로 사용할 수 있다.

랭체인이 제공하는 주요 인터페이스는 다음과 같다.
  • Prompt: Dictionary 유형 프롬프트 정의
  • Model: LLM 모델
  • OutputParser: 모델 출력
  • Retriever: RAG 처리
이외, 랭체인은 비동기 병렬 처리인 ainvoke, abatch, astream_event를 제공한다. 이벤트는 체인을 구성하는 파이프의 실행 시작, 종료 등에 따라 콜백함수 호출을 제공한다. 파이프 입출력은 Pydantic으로 체크, 생성되므로, 데이터 무결성이 보장된다(참고. Cerberus). 

사용자는 랭체인에 등록된 에이전트에게 LLM을 통해 삭제를 지시할 수 있다. 이 경우 중요한 정보가 삭제될 수 있으므로, 이에 대한 권한을 부여하거나, 에이전트가 컨테이너 샌드박스에서 실행될 수 있도록 제한 할 수 있다.

설치하기
터미널에서 다음 명령을 실행한다.
pip install langchain

상세 내용은 다음을 참고한다. 

기본 사용법
여기서는 OLLAMA를 설치하여, LLAMA2를 모델로 사용한다. 만약, ChatGPT 모델을 이용한다면, OpenAI에 API Token을 신청(유료)해야 한다(다음 소스코드에서 해당 모델로 변경하면 됨). 여기서는 프롬프트를 입력해 원하는 결과를 얻는 방법을 코드로 보여준다. 
다음같이 올라마를 설치한다. 
다음과 같이 LLAMA2학습 모델을 터미널에서 다운로드한다.
ollama pull llama2

다음과 같이 코딩한다.
# export LANGCHAIN_TRACING_V2="true"
# export LANGCHAIN_API_KEY="..."
# from langchain_openai import ChatOpenAI # ChatGPT 사용 시 라이브러리
from langchain_community.llms import Ollama  

llm = Ollama(model="llama2")  # 라마2 모델 사용. 챗GPT사용의 경우, llm = ChatOpenAI(model="gpt-4", openai_api_key="your-api-key")

out = llm.invoke("what is BIM?")  #프롬프트 질문
print('llm. ', out)

from langchain_core.prompts import ChatPromptTemplate

# 프롬프트 질문 > Agent A 입력 > 출력 > Agent B 입력... 이런식으로 조합할 수 있음
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are world class technical documentation writer."),
    ("user", "{input}")
])  

chain = prompt | llm 
out = chain.invoke({"input": "what is BIM?"})
print('\n\n*prompt | llm. ', out)

from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()

chain = prompt | llm | output_parser
out = chain.invoke({"input": "what is BIM?"})
print('\n\n*prompt | llm | output_parser. ', out)

결과는 다음과 같다. 

여기서 사용한 프롬프트는 기본 템플릿을 사용했지만, 채팅, 코딩, Few Shot QA, 요약 등 목적에 따라 매우 다양한 템플릿을 제공한다. 다음에는 그 예이다. 
chat_template = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful AI bot. Your name is {name}."),
        ("human", "Hello, how are you doing?"),
        ("ai", "I'm doing well, thanks!"),
        ("human", "{user_input}"),
    ]
)

example_prompt = PromptTemplate(
    input_variables=["question", "answer"], template="Question: {question}\n{answer}"
)

LCEL(LangChain) 언어 기반 서비스 개발
LangChain Expression Language인 LCEL을 지원한다. 이를 통해, 100개 단계 이상 연결된 랭체인이 배포될 수 있다. LCEL언어는 유닉스 파이프라인과 유사하다. 

이 언어는 스트리밍, 비동기, 병렬실행을 지원한다. LangSmith를 통해 각 단계 상황을 추적할 수 있다. LangServe를 통해 체인 배포가 가능하다.

실습을 위해, 다음 명령을 터미널에서 실행한다.
pip install langchain-core langchain-community langchain-openai
 
다음을 코딩한다. ChatGPT4 사용의 경우, 주석의 model과 key는 적절히 수정하라.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.llms import Ollama
# from langchain_openai import ChatOpenAI # ChatGPT 사용 시 라이브러리

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}") # 변수정의

model = Ollama(model="llama2") # ChatOpenAI(model="gpt-4", openai_api_key="your-api-key")
output_parser = StrOutputParser()

chain = prompt | model | output_parser  # LCEL 파이프라인
out = chain.invoke({"topic": "ice cream"})  # 전달 변수
print(out)

결과는 다음과 같다. ice cream에 맞는 적당한? 조크를 한다.

이 코드는 topic 변수를 파이프라인에 전달함으로써, 프롬프트 입력에 대한 LLM모델의 출력에 가변성을 주고 있다. 

아래 코드는 동일한 invoke 동작을 한다.
(prompt | model | output_parser).invoke({"topic": "ice cream"})  # 전달 변수

다음과 같이, chain.batch 이용해 배치로 실행할 수 있다.
chain.batch(['ice cream', 'spaghetti'])

RAG 기반 개인화된 에이전트 개발
일반 상식적인 대답보다는 좀 더 전문적이고 개인화된 결과를 생성할 수 있다. 이는 RAG(Retrieval Augmented Generation. 검색증강생성)으로 알려진 기술을 통해 개선될 수 있다. 

RAG는 LLM에 프롬프트를 전달하기 전에 미리 저장된 지식 데이터베이스에서 보강된 컨텍스트를 미리 전달하고, 이 문맥을 고려한 프롬프트 쿼리를 수행하는 절차를 수행한다. 다음은 그 과정을 간략히 보여준다. 
OpenAI의 샘알트만 사건을 왕좌의 게임과 비교한 질문 예시

RAG는 LLM의 자연스로운 출력 생성 기능을 최대한 활용하면서, 얻고 싶은 대답에 집중할 수 있는 문맥을 청크들로 제공해주는 기술로 볼 수 있다. 

실습을 위해 다음을 설치한다. 
pip install ollama llamaapi chromadb docarray

RAG를 이용해 랭체인을 코딩해 본다. 
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings

vectorstore = DocArrayInMemorySearch.from_texts(
    ["Mac worked at Seoul", "Mac is researcher who develop AI, 3D computer vision, BIM and Digital Twin", "Mac manages BIM principle and Daddy Makers Blog", "Lynn Minmay is an idol in Macross Movie", "Apple makes Macintosh"],
    embedding=OllamaEmbeddings(), 
) # RAG vector store에 질문 맥락 설정. Mac단어가 포함된 다른 정보도 함께 제공
retriever = vectorstore.as_retriever()
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)

template = """Answer the question based only on the following context:
{context}
Question: {question}
"""  # 템플릿 변수 정의
prompt = ChatPromptTemplate.from_template(template) # 프롬프트 생성
model = Ollama(model="llama2")
output_parser = StrOutputParser()

chain = setup_and_retrieval | prompt | model | output_parser
out = chain.invoke("where did Mac work and what is his job?") # RAG기반 질문
print(out)  

이를 실행하면, 다음과 같이 정확한 답을 생성한다.

코딩 생성 서비스 개발
랭체인은 템플릿을 이용해 코딩 생성 서비스를 개발할 수 있다. 
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import (
    ChatPromptTemplate,
)
from langchain_experimental.utilities import PythonREPL
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings


template = """Write some python code to solve the user's problem. 
Return only python code in Markdown format, e.g.:
```python
....
```""" # ㅌ템플릿 변수 정의
prompt = ChatPromptTemplate.from_messages([("system", template), ("human", "{input}")])
model = Ollama(model="llama2")
def _sanitize_output(text: str):  # 출력 포맷
    _, after = text.split("```python")
    return after.split("```")[0]

chain = prompt | model | StrOutputParser() | _sanitize_output | PythonREPL().run

input = {"input": "whats 2 plus 2"}  # 파이썬 코드 질문
out = chain.invoke(input)
print(input, '=', out)

이 코드를 실행하면, 다음 결과를 얻을 수 있다.

QA 서비스 개발
랭체인은 PromptTemplate을 이용해 편리하게 QA 서비스를 개발할 수 있다.
from langchain.prompts import PromptTemplate

template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Use three sentences maximum. Keep the answer as concise as possible. Always say "thanks for asking!" at the end of the answer. 
{context}
Question: {question}
Helpful Answer:"""

QA_CHAIN_PROMPT = PromptTemplate.from_template(template)# Run chain
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

마무리
랭체인은 다양한 학습 모델 에이전트들을 이용해 사용자가 원하는 결과를 생성하는 데 특화된 라이브러리이다. 이를 이용해, 다양한 인공지능 에이전트 서비스를 개발할 수 있다.

참고: Pinecore 벡터DB
임베딩된 데이터 벡터를 저장, 검색, 관리하는 데이터베이스는 여러가지가 있다. Pinecore는 그 중에 하나이다. 

참고: 파인튜닝, RAG, VectorDB
레퍼런스