태그 보관물: Python

LangChain: Tool들을 사용하는 Agent

인공지능 Agent는 사용자의 요청을 받은 후 어떤 기능을 어떤 순서로 실행할지 결정하는 역할을 합니다. Agent의 주요 특징들은 다음과 같습니다.

  • 목표(정책) 기반 행동: 주어진 목표를 달성하기 위해 행동합니다.
  • 자율성: 목표가 주어지면 자동으로 작동합니다.
  • 감지: 주변 환경에서 정보를 수집합니다.

Tools는 Agent가 할 수 있는 일에 해당합니다. 도구, 기술, 함수들이죠.

Agent의 작업 처리 순서도

Agent의 처리 흐름은 다음과 같습니다.

  1. Input: 사용자가 Agent에게 작업을 할당합니다.
  2. Thought: Agent가 작업을 완수하기 위해 무엇을 할지 생각합니다.
  3. Action/Action Input: 사용할 도구를 결정하고, 도구의 입력(함수의 입력값)을 결정합니다.
  4. Observation: 도구의 출력 결과를 관찰합니다.
  5. 관찰 결과 작업을 완료(Finish)했다는 판단에 도달할 때까지 2~4번 과정을 반복합니다.

위의 과정이 Agent의 작업 수행 과정인데, LangChain에서는 이 과정을 처리하는 객체를 Agent Executor라고 합니다.

LangChain Agent의 종류

LangChain에는 여러 종류의 Agent들이 있습니다. 몇 가지를 살펴보겠습니다.

  • Zero-shot ReAct: 작업과 도구 설명을 보고 사용할 도구를 결정합니다. ReAct는 Reasoning + Acting으로 Yao et al. (2022)의 논문에 소개된 개념입니다. Reasoning은 생각(Chain-of-Thought, COT), Acting은 외부 도구 실행을 의미합니다.
  • Structured input ReAct: input이 여러 개인 도구를 사용할 때 필요한 Agent입니다.
  • Conversational: 대화 + ReAct로 대화 저장을 위한 메모리가 필요합니다.
  • Self-ask with search: 인터넷 검색 후 답변하는 Agent로 검색 도구가 필요합니다.
  • ReAct document store: 문서 저장소 + ReAct로 문서 검색 도구가 필요합니다.

필요한 작업에 따라 정하면 되는데, Zero-shot ReAct가 가장 기본이 되는 Agent입니다.

LangChain Tools

LangChain Tools는 Agent가 수행하는 특정 기능으로, 외부 환경에 영향을 끼쳐 Agent가 새로운 정보를 관찰할 수 있도록 합니다. 도구를 추가할 때마다 LLM에 새로운 능력을 부여하는 셈이죠. 영화 매트릭스에서 트리니티가 순식간에 헬리콥터 조종 기술을 다운받는 것처럼 LLM에게도 순식간에 새로운 능력을 가르칠 수 있습니다. LangChain에는 미리 준비된 다양한 도구들이 존재하고, 필요하면 직접 도구를 만들어 쓸 수도 있습니다. LangChain의 기본 도구들을 몇 가지 살펴보겠습니다.

  • 일반적인 검색 도구: Google, Bing, DuckDuckGo
  • 특수 목적 검색 도구: Wolfram Alpha, Wikipedia, Youtube, Weather, News
  • 수학 계산: Math
  • 인터넷 RestAPI: Requests
  • 컴퓨터: Shell, Python REPL

이 외에도 여러 가지 도구들이 계속 추가되고 있습니다. 도구들 중에는 별도의 API 키가 필요한 도구도 있고, 무료로 사용할 수 있는 도구들도 있습니다. API 키가 필요한 도구들은 사용량에 따라 해당 서비스 제공자에게 사용료를 지불해야 할 수도 있습니다. 또한 도구들 중에는 실행을 위해 LLM이 필요한 도구도 있고 필요 없는 도구도 있습니다. LLM이 필요한 도구는 사용시 LLM을 지정해줘야 합니다.

예제: Math, Wikipedia

예제 코드를 보겠습니다.

!pip install langchain openai wikipedia

from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent

llm = ChatOpenAI(temperature=0)
tools = load_tools(["llm-math","wikipedia"], llm=llm)
# llm-math 도구는 llm이 필요합니다.

agent= initialize_agent(
    tools,
    llm,
    agent='zero-shot-react-description',
    handle_parsing_errors=True,
    verbose = True)

# 수학 문제
agent("500의 20.5%는 몇인가요?")
# 결과: {'input': '500의 20.5%는 몇인가요?', 'output': '102.5'}

# wikipedia 검색 문제
question = "광안대교 길이는 몇 km인가요?"
result = agent(question)
# 결과: {'input': '광안대교 길이는 몇 km인가요?', 'output': 'The length of the Gwangan Bridge is 7,420 meters.'}

LangChain 내부 프롬프트들이 영어로 되어 있어서 응답 결과는 기본적으로 영어로 나옵니다. Wikipedia 도구는 Wikipedia에서 검색한 페이지의 내용을 모두 가져오는게 아니라 요약 부분만 가져오기 때문에 요약 부분에 원하는 내용이 없으면 질문에 대한 답을 못 찾을 수도 있습니다. 그럴땐 Python wikipedia 라이브러리를 이용해 페이지 전체를 받아서 답을 찾는 사용자 정의 도구를 만들거나 다른 검색 도구를 사용할 수 있습니다. 다음 예제에서 DuckDuckGo 검색엔진을 사용해봅시다.

예제: DuckDuckGo Search

! pip install duckduckgo-search

tools = load_tools(["ddg-search"])
agent= initialize_agent(
    tools,
    llm,
    agent='zero-shot-react-description',
    handle_parsing_errors=True,
    verbose = True)

result = agent(question)
# 결과: {'input': '광안대교 길이는 몇 km인가요?', 'output': 'The length of the Gwangan Bridge is 7.4 kilometers.'}

Google이나 Bing은 API Key가 필요하지만, DuckDuckGo는 API Key 없이 사용할 수 있는 검색엔진입니다.

예제: Youtube

이번엔 인터넷 검색에 유튜브 검색까지 같이 해봅시다.

! pip install youtube_search

from langchain.tools import YouTubeSearchTool
tools = load_tools(["ddg-search"]) + [YouTubeSearchTool()]

agent= initialize_agent(
    tools,
    llm,
    agent='zero-shot-react-description',
    handle_parsing_errors=True,
    verbose = True)

result = agent("이퀼리브리엄 영화에서 주인공이 저항군 아지트에서 들은 클래식 음악이 무엇인지 찾아서 유튜브 링크를 알려줘")

### 길지만 이해를 위해 중간 과정을 살펴 봅시다.
Entering new AgentExecutor chain...
- Thought: I need to find the name of the classical music heard by the protagonist in the movie "Equilibrium" and provide a YouTube link.
- Action: duckduckgo_search
- Action Input: "Equilibrium movie protagonist classical music"
- Observation: What classical music is played in the movie 'Equilibrium'? The character of Preston listens to Ludwig van Beethoven 's 9th Symphony, First Movement that he accidentally activates on the Victrola. If the audio is a recording of the actual song please check the replies to this comment to see if the bots u/RecognizeSong, u/songfinderbot and u/find-song identified it correctly :) (might take a few seconds to reply) . Please flair your post as Answered if the bots got it right, or comment to say if they listed the wrong song(s), so that people would know if help is still needed. What classical music is played in the movie 'Equilibrium'? The character of Preston listens to Ludwig van Beethoven's 9th Symphony, First Movement that he accidentally activates on the Victrola. What classical music is played in the movie 'Equilibrium'? The character of Preston listens to Ludwig van Beethoven's 9th Symphony, First Movement that he accidentally activates on the Victrola. Equilibrium hit theaters on Dec. 6, 2002, with a budget of $20 million. It only made $5 million back. That's despite a positive review from Roger Ebert, who praised the film's blend of ...
- Thought:I have found the name of the classical music heard in the movie "Equilibrium" and it is Ludwig van Beethoven's 9th Symphony, First Movement.
- Action: youtube_search
- Action Input: "Ludwig van Beethoven 9th Symphony First Movement,1"
- Observation: 'https://www.youtube.com/watch?v=VKxX2EJozxo&pp=ygUwTHVkd2lnIHZhbiBCZWV0aG92ZW4gOXRoIFN5bXBob255IEZpcnN0IE1vdmVtZW50'
- Thought:I now know the final answer.
- Final Answer: The classical music heard by the protagonist in the movie "Equilibrium" is Ludwig van Beethoven's 9th Symphony, First Movement. You can listen to it on YouTube at this link: [https://www.youtube.com/watch?v=VKxX2EJozxo&pp=ygUwTHVkd2lnIHZhbiBCZWV0aG92ZW4gOXRoIFN5bXBob255IEZpcnN0IE1vdmVtZW50]
- Finished chain.

# 최종 반환값입니다.
{'input': '이퀼리브리엄 영화에서 주인공이 저항군 아지트에서 들은 클래식 음악이 무엇인지 찾아서 유튜브 링크를 알려줘',
 'output': 'The classical music heard by the protagonist in the movie "Equilibrium" is Ludwig van Beethoven\'s 9th Symphony, First Movement. You can listen to it on YouTube at this link: [https://www.youtube.com/watch?v=VKxX2EJozxo&pp=ygUwTHVkd2lnIHZhbiBCZWV0aG92ZW4gOXRoIFN5bXBob255IEZpcnN0IE1vdmVtZW50]'}

앞에 나온 순서도의 중간 과정을 두 번 반복했습니다. 먼저 DuckDuckGo 검색을 사용하고 그 결과를 이용해 두 번째에는 Youtube 검색으로 최종 결과를 얻었습니다. 만약 Youtube 링크가 주어졌을 때 영상을 틀어주는 도구를 추가한다면 바로 음악을 틀어달라고 요청할 수도 있겠죠? 이번엔 사용자 지정 도구를 만들어 봅시다.

예제: 사용자 도구 만들기

사용자 도구는 LangChain의 tool 데코레이터로 함수를 꾸며주면 됩니다. 함수 설명 문자열에 함수의 기능과 입력 인자 정보를 넣어야 Agent에서 해당 내용을 보고 어떤 도구인지, 어떻게 사용하는지 알 수 있습니다. OpenAI API의 Function Call에서 함수와 인자의 description에 해당하는 부분이죠. LLM이 사용하는 도구이므로 함수의 입력과 출력은 모두 문자열로 해줍니다.

from langchain.agents import tool
from datetime import date

@tool
def date_today(text):
  """오늘 날짜 반환, input은 ''"""
  return str(date.today())

agent= initialize_agent(
    [date_today],
    llm,
    agent='zero-shot-react-description',
    handle_parsing_errors=True,
    verbose = True)

agent("오늘 날짜는?")
{'input': '오늘 날짜는?', 'output': '오늘 날짜는 2023-10-26입니다.'}

이렇게 OpenAI API의 함수 호출 기능에 비해 비교적 간단히 LLM에게 새로운 기능을 추가해줄 수 있습니다.

OpenAI 임베딩과 벡터 거리

OpenAI 임베딩 모델

OpenAI API를 이용해 문자열을 임베딩 벡터로 변환해보겠습니다. 임베딩 모델에 몇 가지가 있지만 그 중 OpenAI에서 권장하는 text-embedding-ada-002 모델을 사용하면 됩니다. 모델 정보는 아래 표에 정리했습니다.

토크나이저cl100k_base
최대 입력 토큰수8192
출력 벡터 차원1536
API 사용료$0.0001/1K tokens
text-embedding-ada-002 모델

파이썬 API

임베딩 계산을 위한 파이썬 코드는 다음과 같습니다.

import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")

text = "I love Python!"

response = openai.Embedding.create(
    input = text,
    model = "text-embedding-ada-002"
)

결과로 나오는 response는 다음과 같습니다.

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [
        -0.0078111737966537476,
        -0.01599879004061222,
        -0.0008188456413336098,
        -0.03275046497583389,
        -0.01546101551502943,
        ...
        0.027292054146528244,
        -0.0034115067683160305,
        -0.005542438011616468,
        -0.006056685000658035,
        -0.03979530930519104
      ]
    }
  ],
  "model": "text-embedding-ada-002-v2",
  "usage": {
    "prompt_tokens": 4,
    "total_tokens": 4
  }
}

결과 임베딩 벡터는 1536차원이기 때문에 중간은 생략했습니다. response에서 임베딩 벡터만 추출하려면 다음과 같이 합니다.

embeddings = response['data'][0]['embedding']

벡터 거리와 의미 검색

텍스트를 임베딩 벡터로 변환하면 벡터들 사이의 거리를 계산할 수 있게 됩니다. 두 임베딩 벡터의 거리가 가까우면 의미가 유사한 텍스트, 벡터의 거리가 멀먼 관련 없는 텍스트라고 할 수 있습니다. 따라서 텍스트 사이에 일치하는 단어가 없더라도 벡터 거리를 이용해 관련된 텍스트를 찾아낼 수 있죠. 의미 검색(Semantic Search)에서 사용하는 방법입니다.

코사인 유사도와 코사인 거리

벡터 사이의 거리를 계산하는 방법에는 여러 가지가 있는데, LLM에서 많이 사용하는 방법은 코사인 유사도(Cosine Similarity)를 이용하는 방법입니다.

S_C \left(\mathbf{A}, \mathbf{B} \right) = \frac{\mathbf{A} \cdot \mathbf{B}}{\lVert\mathbf{A}\rVert \lVert \mathbf{B} \rVert}

코사인 유사도는 두 벡터가 가까우면 1, 멀면 0이 됩니다. 거리를 재려면 가까운게 0, 먼게 1이 되도록 하는게 좋겠죠? 코사인 거리(Cosine Distance)는 다음과 같이 계산합니다.

D_C \left( \mathbf{A},\mathbf{B}  \right) = 1- S_C \left( \mathbf{A},\mathbf{B}  \right)

예제

I love Python!“에서 한 단어씩만 바꾼 문장을 몇 개 만들어서 임베딩 벡터를 만들고, 원래 문장의 임베딩 벡터와 코사인 거리를 계산해보겠습니다. 사용할 문장들입니다.

  • I love Python (느낌표 삭제)
  • I like Python!
  • I hate Python!
  • I love JavaScript!
  • I love you!

다음은 테스트에 사용한 함수들입니다. 먼저 임베딩 벡터를 한 번에 얻는 get_embedding 함수를 만들고 코사인 유사도와 코사인 거리를 계산하는 함수를 만들었습니다.

import numpy as np
from numpy.linalg import norm

def get_embedding(text):
    response = openai.Embedding.create(
        input=text,
        model="text-embedding-ada-002"
    )
    return np.array(response['data'][0]['embedding'])

def cosine_similarity(a,b):
    return np.dot(a,b) / (norm(a)*norm(b))

def cosine_dist(a,b):
    return 1-cosine_similarity(a,b)

테스트 코드입니다.

text_org = "I love Python!"
texts = ["I love Python",
        "I like Python!",
        "I hate Python!",
        "I love JavaScript!",
        "I love you!"]

# 임베딩 벡터 구하기
emb_org = get_embedding(text_org)
emb_comp = [get_embedding(t) for t in texts]

# 코사인 거리 계산
dist = [cosine_dist(emb_org, emb) for emb in emb_comp]

# 결과 출력
for t,d in zip(texts,dist):
    print(f"{d:.4f}: {t}")

다음은 출력된 결과입니다.

0.0235: I love Python
0.0164: I like Python!
0.0790: I hate Python!
0.0888: I love JavaScript!
0.1631: I love you!

결과를 관찰해 봅시다. 숫자는 옆에 있는 문장이 기준 문장 “I love Python!“과 얼마나 가까운 문장인지 나타냅니다. 값이 작을수록 의미가 더 가깝죠.

  • “I like Python!”이 “I love Python”보다 더 가깝습니다. love/like 차이보다 느낌표 유무의 차이가 더 크네요!
  • hate는 당연히 like에 비해 love로부터의 거리가 멀죠.
  • 하지만 “I hate Python!”은 “I love JavaScript!” 보다는 가깝습니다. 좋던 싫던(정반대의 의미지만) 파이썬 선호도를 나타낸 문장들끼리는 JavaScript 선호도를 나타낸 문장보다 의미가 가깝네요.
  • JavaScript가 Python은 아니지만 둘 다 프로그래밍 언어이기 때문에 “I love JavaScript!”는 “I love you!” 보다 의미가 훨씬 가깝습니다.

이렇게 텍스트를 임베딩 벡터로 만들면 벡터 사이의 거리를 이용해 의미가 가까운 정도를 정량적으로 계산할 수 있습니다. 벡터 데이터베이스를 이용하면 데이터베이스에 저장해놓은 수많은 임베딩 벡터들 중 기준이 되는 문장(query)과 거리가 가까운 벡터들을 빠르게 추출할 수 있습니다.

환경 변수와 파이썬 dotenv

환경 변수 Environment Variables

환경 변수는 컴퓨터 운영체제 또는 쉘 차원에서 관리하는 변수로, 환경 변수를 이용하면 서로 다른 프로세스 간에 변수를 공유할 수 있습니다. 환경 변수는 언어, 시간대 같은 시스템 설정, 프로그램의 동작을 변경하는 설정, API 키, 프로그램 경로 등 다양한 목적으로 사용할 수 있습니다. 윈도우즈 운영체제에서는 환경 변수 설정 메뉴를 이용해, 리눅스나 맥OS에서는 쉘에서 export 명령을 이용해 설정할 수 있습니다.

파이썬에서 환경 변수 다루기

파이썬에서는 os 모듈environ 또는 getenv를 사용해 환경 변수에 접근할 수 있습니다.

import os

# 읽기
val = os.environ["MY_VARIABLE"]
val = os.environ.get("MY_VARIABLE")
val = os.getenv("MY_VARIABLE")

# 쓰기
os.environ["MY_VARIABLE"] = val

os.environ을 이용하면 환경 변수를 딕셔너리처럼 다룰 수 있습니다. 존재하지 않는 환경 변수에 접근하려고 하면 에러가 발생합니다. 이 때 get 메소드를 이용하면 변수가 존재하지 않을 때 사용할 default 값을 지정해줄 수 있습니다. 별도로 default 값을 지정하지 않으면 환경 변수가 존재하지 않을 때 None을 반환합니다. os.getenvos.environ.get과 동일하게 동작합니다. 보통 좀 더 간결한 os.getenv를 사용합니다.

os.environ을 이용해 설정한 환경 변수는 현재 실행 중인 프로세스에만 영향을 미치고, 시스템 전체 환경 변수를 변경하지는 않습니다.

Colab에서 환경 변수

API 키와 같은 비밀번호의 경우 코드 내에 저장하는 것은 보안 문제로 인해 보통 추천하지 않습니다. API 키는 환경 변수에 저장하고 사용하는 것이 좋습니다. 하지만, Colab과 같이 직접 환경 변수를 지정하기 어려운 상황도 있습니다. 이런 경우에는 환경 변수에 저장하는 대신 설정 파일에 API 키를 저장해놓고 코드에서 읽어서 쓸 수 있습니다. 파이썬에는 ini, cfg와 같은 설정 파일을 다루는 configparser 모듈이 있습니다. YAML, TOML이나 JSON 파일을 설정 파일로 쓰는 경우도 있습니다. 아래 살펴볼 dotenv 패키지는 설정 파일에서 읽어들인 값을 환경 변수처럼 이용할 수 있도록 만드는 패키지입니다.

dotenv 패키지

dotenv 패키지는 다음과 같이 설치할 수 있습니다.

pip install python-dotenv

Colab 노트북에서 설치하려면 위의 명령 앞에 느낌표(!)를 붙이면 됩니다. 기본 설정 파일은 .env 파일입니다. 파일 내용은 다음과 같이 적습니다.

TEMPERATURE=0.8
OPENAI_API_KEY=sk-my-key

위 변수들은 같은 디렉토리에서 다음의 코드로 읽어들일 수 있습니다.

import os
from dotenv import load_dotenv
load_dotenv()

temperature = float(os.getenv("TEMPERATURE"))
openai_api_key = os.getenv("OPENAI_API_KEY")

만약 .env 파일이 다른 경로에 있다면 load_dotenv(dotenv_path="/path/to/dotenv")와 같이 절대 또는 상대 경로를 넣어주면 됩니다. 이미 동일한 이름의 시스템 환경 변수가 존재하는 경우 .env 파일에 적은 환경 변수는 무시되는데, 시스템 환경 변수를 덮어쓰고 싶으면 override=True 옵션을 주면 됩니다. 단순히 API 키나 변수를 읽어들이는 경우 위의 코드로 충분합니다. 다른 기능이 필요할 경우 여기에서 load_dotenv 함수의 다른 인자와 패키지 내의 다른 함수 설명을 찾을 수 있습니다.

참고로, dotenv로 읽어들인 환경 변수는 모두 문자열(str) 자료형이 됩니다. 따라서 필요할 경우 적절히 형변환을 해서 사용해야 합니다. 불리언 값의 경우 .env 파일에 적은 TRUE, FALSE 값은 파이썬 문자열로 변환된 후 bool("FALSE")와 같이 쓰면 모두 True 값이 되니 주의해야 합니다.

파이썬에서 json 파일 다루기

JSON 형식

JSON (JavaScript Object Notation) 파일은 인터넷에서 서버와 클라이언트 사이 통신에 많이 사용하는 텍스트 표기 형식입니다. REST API에서도 많이 사용하고, 설정 파일로 사용하기도 하죠. 다음과 같이 사람과 컴퓨터가 읽을 수 있는 텍스트 형식입니다.

{
    "key": "value",
    "list": [
        {"str": "text string"},
        {
            "number": {"int": 1234, "float": 12.34}
        },
        {"True": true},
        {"False": false},
        {"None": null}
    ]
}

JSON에는 { }로 표기하는 객체(object), [ ]로 표기하는 배열(array), 큰 따옴표로 감싸는 문자열(string), 숫자(number), 불리언(boolean), null 자료형이 있습니다. 객체는 키와 값으로 이루어지는데, 키는 문자열이고, 값은 위의 6가지 자료형 중 하나가 될 수 있습니다. 객체 안에 객체를 넣는 것도 가능합니다. 배열에도 여러 자료형들이 함께 들어갈 수 있습니다.

파이썬에서 JSON 읽기

파이썬에서 JSON 파일을 읽으려면 다음과 같이 json 모듈load 함수를 사용하면 됩니다.

import json
with open('data.json') as fj:
    data = json.load(fj)

문자열(str 또는 bytes, bytearray)에 json 내용이 들어있을 경우 loads를 이용합니다.

import json
js = '{"key": "value"}'
data = json.loads(js)

이렇게 읽어들인 data는 파이썬에서 dict 자료형이 됩니다. JSON 자료형은 읽었을 때 아래와 같이 파이썬 자료형으로 변환됩니다.

JSONPython
objectdict
arraylist
stringstr
number (integer)int
number (real)float
trueTrue
falseFalse
nullNone
JSON to Python

파이썬에서 JSON 쓰기

파이썬에서 JSON 파일을 쓰려면 dump, 문자열에 쓰려면 dumps 함수를 이용합니다.

import json
data = ["list", {'key': 'value'}]

with open('data.json', 'w') as fj:
    json.dump(data, fj)

js = json.dumps(data, indent=4)

dumps에서 indent 옵션을 넣으면 출력시 보기 좋게 만들어줍니다. 파이썬 자료형은 아래와 같이 JSON 자료형으로 변환됩니다.

PythonJSON
dictobject
list, tuplearray
strstring
int, float, (int, float 열거형)number
Truetrue
Falsefalse
Nonenull
Python to JSON

JSON 파일에 한글 쓰기

파이썬 문자열에 한글이 포함되어 있을 경우 위의 dump, dumps를 이용해 JSON 파일이나 문자열을 쓰면 한글이 바로 표시되지 않습니다. 그래도 load, loads로 읽으면 제대로 읽어지기는 합니다. 파일이나 문자열로 썼을 때 한글이 바로 표시되게 만들고 싶으면 dump, dumpsensure_ascii=False 옵션을 주면 됩니다.

import json
data = ["list", {'key': '한글'}]

with open('data.json', 'w') as fj:
    json.dump(data, fj, ensure_ascii=False)

js = json.dumps(data, indent=4, ensure_ascii=False)

제어 문자가 들어간 문자열

\n, \t, \r, \0과 같은 제어 문자가 들어간 문자열을 json.loads로 읽어들이려고 하면 JSONDecodeError가 발생합니다. 제어 문자를 포함하고 싶으면 strict=False 옵션을 주면 됩니다.

js = """
["list",
{"key": "        
한글        
"}]
"""

data = json.loads(js, strict=False)

속도 프로파일과 탄성파 트레이스 추출하여 그리기

속도모델에서 프로파일을 추출하여 깊이에 따라 속도 그림을 그려보겠습니다. 이진 형식의 속도파일에서 텍스트 파일로 프로파일을 추출한 후 그리는 방법과 이진 속도파일을 직접 읽어서 그리는 방법을 살펴보겠습니다. 참고로, 탄성파 공통송신원모음 등에서 트레이스를 추출하여 그리는 과정 또한 동일합니다.

텍스트 파일로 추출하여 그리기

바이너리 파일에서 프로파일 또는 트레이스를 추출하기 위해 gpl 라이브러리의 gplTracePick 프로그램을 사용하겠습니다. 이차원 단면(속도모델, 공통송신원모음 등)에서 세로 방향 트레이스를 추출할 때 사용하는 프로그램입니다. (가로방향 트레이스는 gplHTracePick 프로그램을 이용하면 됩니다.) 이 프로그램을 그냥 실행하면 아래와 같은 도움말이 나옵니다.


%%sh # 이 글을 쓰고 있는 jupyter notebook에서 shell 명령을 실행하기 위한 magic command입니다.
gplTracePick # 실제 터미널상에서 실행하는 명령어

 Gpl trace picker
 Required parameters:
     [i] n1=            : # of grids in fast dimension
     [s] fin=           : input binary file
     [s] fout=          : output binary file
     [i] pick=          : (=first), first pick (1~n2)
 Optional parameters:
     [i] last=first     : last pick (pick~n2)
     [i] step=1         : pick step
     [f] d1=1.0         : grid size
     [i] n2=calc        : # of grids in slow dimension
     [s] type=f         : data type [ifdcz]
     [s] otype=a        : output type [ab] (ascii/binary)

위에서 n1finfoutpick은 프로그램 실행시 필수적으로 넣어줘야 하는 값입니다.

  • n1은 세로 방향(fast dimension) 격자수
  • fin은 입력 파일 이름
  • fout은 출력 파일 이름
  • pick은 추출하고자하는 가로 방향(slow dimension) 격자 번호입니다. 격자 번호는 1번부터 시작합니다.

Marmousi 속도모델(nx=576, ny=188, h=0.016 km)에 대해 1.6 km 지점(격자번호 101)에서 시작하여 3.2 km 간격(200개 격자 간격)으로 3개의 속도 프로파일을 추출한다면 아래와 같이 실행할 수 있습니다.


%%sh
gplTracePick n1=188 d1=0.016 fin=marm16km.bin fout=vel_profile.txt pick=101 step=200 last=501

 n2=         576


     n1=188
     d1=0.016
     fin=marm16km.bin
     fout=vel_profile.txt
     pick=101
     step=200
     last=501

그 때 결과물은 아래와 같습니다. 첫 번째 열은 깊이 정보, 두 번째부터 네 번째 열까지는 추출한 속도 프로파일 정보입니다(1.6 km, 4.8 km, 8.0 km).

%%sh
head vel_profile.txt
   0.00000000       1.50000012       1.50000012       1.50000012    
   1.60000008E-02   1.50000012       1.50000012       1.50000012    
   3.20000015E-02   1.50000012       1.65800011       1.59800005    
   4.80000004E-02   1.66200006       1.66200006       1.60200012    
   6.40000030E-02   1.66600013       1.66600013       1.60600019    
   8.00000057E-02   1.67000008       1.73999715       1.69000006    
   9.60000008E-02   1.67400002       1.74399781       1.69400012    
  0.112000003       1.67800009       1.61800003       1.69800007    
  0.128000006       1.78200006       1.70200002       1.63200009    
  0.144000009       1.78600013       1.70600009       1.63600004    

텍스트 파일로 추출한 결과는 gnuplot과 같은 프로그램을 이용해 빠르게 확인해볼 수 있습니다. 여기서는 파이썬의 Matplotlib을 이용하여 위의 속도 프로파일을 그려보겠습니다.


%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

trc=np.loadtxt("vel_profile.txt")

h=0.016
fs='large'

plt.figure(figsize=[15,5])
for i,ix in enumerate([100,300,500]):
    plt.plot(trc[:,0],trc[:,i+1],label="{0} km".format(ix*h))

plt.legend(loc="upper left",fontsize=fs)
plt.xlabel("Depth (km)",fontsize=fs)
plt.ylabel("Velocity (km/s)",fontsize=fs)

<matplotlib.text.Text at 0x10cc66c88>

png

이진 파일을 직접 읽어서 그리기

이번에는 파이썬에서 이진 형식의 속도모델 파일을 직접 읽어서 그려보겠습니다.


nx=576
ny=188
vel=np.fromfile("marm16km.bin",dtype=np.float32)
vel.shape=(nx,ny)

h=0.016
fs='large'
depth=np.arange(ny)*h

plt.figure(figsize=[15,5])
for ix in [100,300,500]:
    plt.plot(depth,vel[ix,:],label="{0} km".format(ix*h))

plt.legend(loc="upper left",fontsize=fs)
plt.xlabel("Depth (km)",fontsize=fs)
plt.ylabel("Velocity (km/s)",fontsize=fs)

<matplotlib.text.Text at 0x10d13dd30>

png

참고로, 파이썬은 배열 인덱스가 0번부터 시작하기 때문에 가로방향 100, 300, 500번 속도 프로파일을 가져다가 그렸습니다(gplTracePick을 이용하는 앞의 예제에서는 101, 301, 501번 격자 위치에서 추출했죠).

탄성파 트레이스 그리기

공통송신원모음에서 탄성파 트레이스를 추출하여 그리는 과정은 속도모델에서 프로파일을 추출하여 그리는 경우와 동일합니다. 아래는 샘플 개수가 723개, 샘플링 간격 4 ms, 트레이스가 96개인 공통송신원모음 파일(marm3000.bin)에서 31번째와 61번째 트레이스를 그리는 예제입니다.


ntr=96
ns=723
dt=0.004
trc=np.fromfile("marm3000.bin",dtype=np.float32)
trc.shape=(ntr,ns)

fs='large'
time=np.arange(ns)*dt

plt.figure(figsize=[15,5])
for itr in [30,60]:
    plt.plot(time,trc[itr,:],label="trace {0}".format(itr+1))
plt.legend(loc="upper left",fontsize=fs)
plt.xlabel("Time (s)",fontsize=fs)
plt.ylabel("Amplitude",fontsize=fs)
plt.xlim([0,ns*dt])

(0, 2.892)

png

속도모델 그림 그리기

두 가지 방법으로 2차원 속도모델을 그려보겠습니다. 첫 번째 방법은 SU의 psimage를 이용하는 방법, 두 번째는 python의 matplotlib을 이용하는 방법입니다.

psimage로 그리기

첫 번째 방법부터 보겠습니다. psimage는 쉘에서 사용하는 명령어이지만, gpl 라이브러리의 psplot 모듈을 이용하면 python 명령을 통해 간편하게 속도모델을 그릴 수 있습니다. Marmousi 속도모델을 그림으로 그려보겠습니다.


from gpl.psplot import plot

nx=576
ny=188
h=0.016
fin="marm16km.drt"

opt = "n1={0} d1={1} d2={1} d1num=1 lbeg=1.5 lend=5.5".format(ny,h,h)
plot.velocity("marm16km.png", fin, opt)

psimage label1="Depth (km)" legend=1 d2s=0.5 lheight=1.0 lstyle="vertright" label2="Distance (km)" height=1.0 labelsize=8 lwidth=0.1 d1s=0.5 width=2.65  n1=188 d1=0.016 d2=0.016 d1num=1 lbeg=1.5 lend=5.5 < marm16km.drt > marm16km.eps

// adding velocity unit (km/s)

// fixing bounding box

// converting .eps to .png ..

vel(marm16km.png)

velocity_color를 이용해 컬러로 그릴 수도 있습니다.


plot.velocity_color("marm16km_color.png",fin,opt)

psimage label1="Depth (km)" ghls="0.33,0.5,1" bps=24 bhls="0.67,0.5,1" d1s=0.5 lwidth=0.1 whls="0,0.5,1" legend=1 d2s=0.5 lheight=1.0 lstyle="vertright" label2="Distance (km)" height=1.0 labelsize=8 width=2.65  n1=188 d1=0.016 d2=0.016 d1num=1 lbeg=1.5 lend=5.5 < marm16km.drt > marm16km_color.eps

// adding velocity unit (km/s)

// fixing bounding box

// converting .eps to .png ..

vel(marm16km_color.png)

Matplotlib으로 그리기

두 번째 방법은 python의 matplotlib 라이브러리를 이용하는 방법입니다. 이를 위해서는 코드에서 numpy를 이용해 속도모델을 읽어들인 후에 matplotlib으로 그립니다. 속도모델을 그리는 부분은 함수로 작성하였는데, 필요에 따라 수정해서 사용하면 되겠습니다.


%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

def plot_vel(vel, h, figsize=[15,4], unit='km/s', xticks=None, yticks=None, cticks=None, cmap='gray_r', fontsize=20):
    xmax=(vel.shape[0]-1)*h
    ymax=(vel.shape[1]-1)*h

    plt.figure(figsize=figsize)
    plt.imshow(vel.transpose(),extent=(0,xmax,ymax,0),cmap=cmap)

    # x,y labels
    plt.xlabel('Distance (km)',fontsize=fontsize)
    plt.ylabel('Depth (km)',fontsize=fontsize)

    # x,y ticks, tick labels
    plt.tick_params(labelsize=fontsize)
    plt.gca().xaxis.tick_top()
    plt.gca().xaxis.set_label_position("top")
    xticks and plt.xticks(xticks)
    yticks and plt.yticks(yticks)

    # colorbar
    cb=plt.colorbar(shrink=1.0,pad=0.01,aspect=10,ticks=cticks)
    plt.clim([vel.min(),vel.max()])
    cb.set_label(unit,fontsize=fontsize)
    ct=plt.getp(cb.ax,'ymajorticklabels')
    plt.setp(ct,fontsize=fontsize)

# 속도모델 읽기
vel=np.fromfile(fin,dtype=np.float32)
vel.shape=(nx,ny)

yticks=[0,1,2] # y축 ticks
cticks=[2,3,4,5] # colorbar ticks
plot_vel(vel,h,yticks=yticks,cticks=cticks)

png

# 컬러로 그리고(cmap='jet') 파일로 저장하기
plot_vel(vel,h,xticks=[0,3,6,9],cmap='jet')
plt.savefig("vel.png",bbox_inches='tight')

png

결과물로 저장한 vel.png 파일은 다음과 같습니다.

vel(vel.png)

Matplotlib을 이용한 탄성파 자료처리 그림 그리기

이전 글에서는 SU 명령어들을 이용해 탄성파 자료처리 결과 확인용 그림을 그리는 방법을 살펴보았습니다. 이번에는 Python의 Matplotlib을 이용하여 그린 그림 예제들을 보겠습니다. 그림은 IPython Processing 모듈을 이용해 그렸으며, 그릴 때 사용한 코드는 github에서 볼 수 있습니다.

속도모델, 구조보정 영상

우선, 다음과 같이 이진파일로부터 2차원 속도모델과 구조보정 결과를 그릴 수 있습니다. 기본적으로 속도모델은 컬러, 구조보정 영상은 흑백으로 그리도록 했지만, 필요에 따라 코드를 수정해서 색상을 바꿀 수 있습니다. 색상을 바꾸고 싶을 경우 imshow 함수의 cmap 인자를 이용하면 됩니다.

%matplotlib inline
from pkprocess import *
import numpy as np

vel = np.fromfile("marm16km.drt", dtype=np.float32)

nx = 576
nz = 188
h = 0.016
vel.shape = (nx, nz)

plot_vel(vel, h)

png


plot_mig(vel,h)

png

공통송신원 모음, 스펙트럼

그리고 SU 파일로부터 공통송신원 모음이나 F-X, F-K 스펙트럼을 그릴 수 있습니다. 공통송신원 모음은 Wiggle trace 또는 이미지로 그릴 수 있고, 이미지 색상은 cmap으로 조절 가능합니다.


su = read_su("marm3000.su")

plot_wiggle(su, perc=97)

min=-616.05078125 max=613.4453125

png


plot_image(su, perc=97)

min=-616.05078125 max=613.4453125

png


plot_image(su, perc=97, cmap='bwr')

min=-616.05078125 max=613.4453125

png


specfx(su)

dt=0.004, fmax=125.0

png


specfk(su)

dt=0.004, fmax=125.0

dx=0.025, kmax=20.0

png

위의 그림들 모두 Matplotlib으로 그렸으므로, 수정이 필요할 경우 Matplotlib 문서를 참고하여 수정해서 사용하시면 되겠습니다.

탄성파 자료처리 그림 그리기

탄성파 자료처리 결과 그림을 쉽게 그리는 방법을 살펴보겠습니다.

탄성파 자료처리를 하다 보면 결과물을 그림으로 확인해야 하는 경우가 많습니다. 특별히 노력해서 그려야 하는 그림도 있지만 속도모델, 공통송신원모음 등 대부분의 그림은 거의 비슷한 명령으로 그릴 수 있습니다. 개인적으로 논문이나 발표자료에 넣을 그림을 그릴 때 Seismic Un*x(SU)를 많이 이용하는데, 몇 가지 자주 그리는 그림들을 쉽게 그릴 수 있도록 파이썬 모듈을 만들었습니다. 모듈은 gpl라이브러리에 포함되어 있습니다. 최근 python 3 용으로 수정하였습니다.

먼저 속도모델을 예로 들어보겠습니다. 그림을 그리기 위한 코드는 다음과 같습니다.


from gpl.psplot import plot

vel="marm16km.drt"
opt="n1=188 d1=0.016 d2=0.016 d1num=1 d2num=2"

plot.velocity_color("vel_color.png",vel,opt)

위 코드는 gpl.psplot 모듈에서 plot을 가져오고, marm16km.drt 파일로부터 opt 문자열의 옵션을 이용하여 vel_color.png 파일을 생성하는데, 컬러로 된 속도모델 그림으로 만들라는 코드입니다.

velocity_color는 그림 종류를 지정하는 명령인데, 현재 다음과 같은 명령들을 지원합니다.

  • velocity(target, source, option, unit=”km/s”)
  • velocity_color(target, source, option, unit=”km/s”)
  • gradient(target, source, option)
  • gradient_color(target, source, option)
  • migration(target, source, option)
  • contour(target, source, option)
  • seismogram(target, source, option)
  • spectrum(target, source, option)

위의 명령들은 SU를 이용해 해당 그림을 그리라는 명령으로, contourpscontour를 사용하고 나머지는 psimage를 사용합니다. 입력 파일이 SU 파일이라면 supscontour 또는 supsimage를 사용합니다.

인자들 중 target은 출력 파일, source는 입력 파일, option은 그림 그릴 때 사용할 옵션입니다. 그림 종류에 따라 기본적으로 몇 가지 옵션이 들어가있는데, n1, d1, d2와 같이 입력 파일에 따라 달라지는 옵션을 option에 넣어주면 됩니다. 그리고 기본 옵션을 덮어쓰고 싶은 경우에도 option에 추가해줍니다.

속도모델의 단위는 기본적으로 km/s로 지정해 놓았는데, 필요에 따라 바꿔서 사용할 수 있습니다. g/cc로 바꾸면 밀도 모델을 그릴 수도 있겠죠. migration은 snapshot을 그릴 때 사용할 수도 있습니다.

SU 명령은 기본적으로 eps 파일을 생성합니다. target을 eps 외의 다른 파일(png, tiff, jpg 등)로 지정하면ImageMagickconvert 명령을 이용해 eps 파일을 변환합니다.

따라서 본 모듈의 모든 기능을 이용하려면 Python, SU, ImageMagick이 필요합니다.

터미널에서 위의 코드를 실행했을 때 나오는 메시지는 다음과 같습니다.

psimage height=1.0 width=2.65 d2s=0.5 lwidth=0.1 lstyle="vertright" lheight=1.0
label2="Distance (km)" ghls="0.33,0.5,1" bps=24 whls="0,0.5,1" legend=1
bhls="0.67,0.5,1" labelsize=8 label1="Depth (km)" d1s=0.5  n1=188 d1=0.016
d2=0.016 d1num=1 d2num=2 < marm16km.drt > vel_color.eps

psimage: bclip=5.5 wclip=1.5

// adding velocity unit (km/s)

// fixing bounding box
Original:  %%BoundingBox: 66 41 353 207
Updated:   %%BoundingBox: 85 104 324 202

// converting .eps to .png ..

내용을 살펴보면 다음 순서로 실행됩니다.

  1. SU의 psimage 명령을 이용해 속도모델 eps 파일을 생성합니다. 옵션은 컬러 속도모델에 맞춰서 들어갑니다. 참고로, 그림 크기는 Geophysics 논문 기준에 맞춘 것입니다.
  2. km/s라는 단위를 넣어줍니다(postscript 수정).
  3. 그림 여백을 조절합니다(bounding box 수정).
  4. eps 파일을 png 파일로 수정합니다.

그리고, 결과물인 vel_color.png은 다음과 같습니다. output(vel_color.png)

아래 코드와 다른 그림 예시를 올리니 필요한 그림에 해당하는 명령을 사용하시면 되겠습니다.


from gpl.psplot import plot

vel="marm16km.drt"
opt="n1=188 d1=0.016 d2=0.016 d1num=1 d2num=2"

plot.velocity("vel.png",vel,opt+"lbeg=1.5 lend=5.5 lfnum=1.5")
plot.velocity_color("vel_color.png",vel,opt)
plot.velocity_color("density_color.png",vel,opt,unit="g/cc")
plot.gradient("grad.png",vel,opt)
plot.gradient_color("grad_color.png",vel,opt)
plot.migration("mig.png",vel,opt)
plot.contour("contour.png",vel,opt)

seismo="marm3000.su"
opt2="f2=0 d2=0.025 d1s=0.5 d2s=0.5"
plot.seismogram("seismo.png",seismo,opt2)

spec="marm3000fx.su"
plot.spectrum("spec.png",spec,opt2)

output(vel.png)

output(vel_color.png)

output(density_color.png)

output(grad.png)

output(grad_color.png)

output(mig.png)

output(contour.png)

output(seismo.png)

output(spec.png)

IPython Processing

학부 탄성파 자료 처리 수업에서 사용하기 위한 파이썬 패키지입니다. 기존의 공개된 Matlab 패키지를 파이썬으로 변환하였습니다. 간단한 2차원 자료 처리(time processing)가 가능합니다.

육상, 해상 자료 처리 예제와 함께 공개하였고, Github에서 받으실 수 있습니다.

참고 문헌

하완수, 2015, 대화식 탄성파 자료 처리 수업을 위한 파이썬 패키지 개발, 한국자원공학회지, 52(4), 414-421.

Postscript bounding box

SU(Seismic Un*x)에 있는 psimage로 Marmousi 속도모델을 그리면 다음과 같습니다.(그림 겉부분의 회색은 그림에 포함되어 있지 않은 부분으로, 경계를 표시하기 위해 넣었습니다.)

eps_bbox_before

여기서 사용한 명령은 다음과 같습니다.

psimage par='../marm8m.txt' label1="Depth (km)" label2="Offset (km)" labelsize=8 height=1.0 width=2.4 legend=1 lstyle=vertright lwidth=0.1 lheight=1 units="m/s" < ../marm8m.drt > marm.eps

이 때, psimage는 그림 주위로 지나치게 넓은 공간을 만들어 줍니다. Bounding box 정보가 정확하지 않기 때문이죠. 이 상태로는 eps 파일을 다른 그림파일로 변환하여 paper에 넣거나 power point 발표자료에 넣기에 좋지 않습니다(물론 자르기 crop 기능을 이용할 수도 있기는 하죠).

이 공간을 없애기 위해서는 아래 명령을 이용합니다.

gs -sDEVICE=bbox -dNOPAUSE -dBATCH marm.eps

그럼 다음과 같은 결과를 보여줍니다.

GPL Ghostscript 8.63 (2008-08-01)
Copyright (C) 2008 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
Loading NimbusSanL-Regu font from /usr/share/fonts/default/Type1/n019003l.pfb… 2656772 1085343 2641408 1357198 2 done.
Loading NimbusSanL-Bold font from /usr/share/fonts/default/Type1/n019004l.pfb… 2673436 1178370 2661504 1363393 2 done.
%%BoundingBox: 87 107 327 200
%%HiResBoundingBox: 87.695997 107.509005 326.645990 199.601994

위의 결과에서 마지막 두 줄에 나온 것이 흰 공간을 없앤 bounding box의 크기입니다. 둘 중 하나를 쓰시면 됩니다. 네 개의 숫자는 각각 왼쪽 아래 x좌표, 왼쪽 아래 y좌표, 오른쪽 위 x좌표, 오른쪽 위 y좌표를 의미합니다. Eps 파일을 텍스트 편집기로 열어서 %%BoundingBox 라고 써진 줄을 찾아 bounding box 크기를 위의 정보로 고쳐주면 아래와 같은 결과를 얻을 수 있습니다.

eps_bbox_after

또는 SU에 있는 psbbox 라는 프로그램을 이용할 수도 있습니다.

psbbox llx=87 lly=107 urx=327 ury=200 < marm.eps >marmfx.eps

Gpl에 있는 fixbbox 프로그램은 위의 과정을 자동으로 실행하는 Python 프로그램으로,

fixbbox <input eps file> <output eps file>

과 같이 실행할 수 있습니다.