2024년 6월 30일 일요일

LangGraph 기반 다중 LLM 에이전트 개발하기

이 글은 Langgraph를 이용해, 간단히 다중 LLM 에이전트를 개발하는 방법을 정리한 것이다.
개발 환경 설정
다음과 같이 개발 환경을 설치한다.
pip install langchain langgraph milvus

RAG 처리 및 에이전트 프롬프트 정의하기
웹 사이트에서 얻은 BIM(Building Information Modeling), GIS, 기술 표준 전문 지식들을 RAG 처리한다. 이후, 에이전트에서 사용할 프롬프트들을 정의한다. 각 프롬프트가 포함된 LangChain Expression Language (LCEL) 체인은 RAG 벡터 데이터베이스에 문서에 대한 QA 질문, 질문 결과에 대한 정확도 및 환각 유무 평가, 만약, 질문에 대한 적절한 대답이 없다면 웹페이지 검색하도록 유도하도록 구성된다.

다음 코드를 입력 후 실행한다. 
import os
from dotenv import load_dotenv
from typing import List
from typing_extensions import TypedDict
from langchain import hub
from langchain.globals import set_verbose, set_debug
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOllama
from langchain.prompts import PromptTemplate
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.output_parsers import StrOutputParser
from langchain_core.output_parsers import JsonOutputParser
from langchain.schema import Document

# setup environment for LLM RAG
load_dotenv()
set_debug(True)
set_verbose(True)

# Load documents
urls = [
"https://www.mdpi.com/2220-9964/7/5/162/",
"https://www.tandfonline.com/doi/full/10.1080/19475683.2020.1743355/",
"https://knowledge.bsigroup.com/articles/harmonize-digitize-and-rationalize-exchange-information-confidently-with-bs/",
]

docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=250, chunk_overlap=0
)
doc_splits = text_splitter.split_documents(docs_list)

# Create vectorstore and retriever
vectorstore = Chroma.from_documents(
documents=doc_splits,
collection_name="BIM_GIS",
embedding=HuggingFaceEmbeddings(),
persist_directory="./Chroma_rag.db",

)
retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3}) # type={similarity_score_threshold, mmr, bm25}, https://wikidocs.net/234016

# Load LLM model using Ollama
local_llm = 'llama3'
llm = ChatOllama(model=local_llm, format="json", temperature=0)

# Test grader prompt with JSON output
prompt = PromptTemplate(
template="""You are a grader assessing relevance 
of a retrieved document to a user question. If the document contains keywords related to the user question, grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. 
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.
Provide the binary score as a JSON with a single key 'score' and no premable or explaination.
 
Here is the retrieved document: 
{document}
Here is the user question: 
{question}
""",
input_variables=["question", "document"],
)

retrieval_grader = prompt | llm | JsonOutputParser()
question = "ISO 19166's BIM to GIS Element Mapping"
docs = retriever.invoke(question, ) # https://www.kaggle.com/code/marcinrutecki/rag-mmr-search-in-langchain
doc_txt = docs[0].metadata if 'description' in docs[0].metadata else docs[0].page_content
answer = retrieval_grader.invoke({"question": question, "document": doc_txt})
print(answer)

# Test QA prompt
prompt = PromptTemplate(
template="""You are an assistant for question-answering tasks. 
Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. 
Use three sentences maximum and keep the answer concise:
Question: {question} 
Context: {context} 
Answer: 
""",
input_variables=["question", "document"],
)

llm = ChatOllama(model=local_llm, temperature=0)

rag_chain = prompt | llm | StrOutputParser() # Chain

question = "ISO 19166's BIM to GIS Element Mapping"
docs = retriever.invoke(question)
answer = rag_chain.invoke({"context": docs, "question": question})
print(answer)

# Test hallucination grader prompt with JSON output
llm = ChatOllama(model=local_llm, format="json", temperature=0)

prompt = PromptTemplate(
template="""You are a grader assessing whether 
an answer is grounded in / supported by a set of facts. Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.
Here are the facts:
{documents} 

Here is the answer: 
{generation}
""",
input_variables=["generation", "documents"],
)

hallucination_grader = prompt | llm | JsonOutputParser()
answer = hallucination_grader.invoke({"documents": docs, "generation": answer})
print(answer)

출력이 다음과 같다면, 성공한 것이다.

LLM 기반 다중 에이전트 구현하기
이제 앞에서 정의된 체인들을 이용해, 에이전트를 정의하고, 그래프를 만든 후 빌드한다.
# Web search tool setup
from langchain_community.tools.tavily_search import TavilySearchResults
tavily_api_key = os.environ['TAVILY_API_KEY'] = <Travily API>
web_search_tool = TavilySearchResults(k=3,  tavily_api_key=tavily_api_key)

# Define the graph state using langgraph
from langgraph.graph import END, StateGraph

class GraphState(TypedDict):
"""
Represents the state of our graph.

Attributes:
question: question
generation: LLM generation
web_search: whether to add search
documents: list of documents 
"""
question : str
generation : str
web_search : str
documents : List[str]

def retrieve(state): # node. Retrieve documents from vectorstore
# Args. state (dict): The current graph state
# Returns. state (dict): New key added to state, documents, that contains retrieved documents
print("---RETRIEVE---")
question = state["question"]
documents = retriever.invoke(question)
return {"documents": documents, "question": question}

def generate(state): # node. Generate answer using RAG on retrieved documents
# Args: state (dict): The current graph state
# Returns: state (dict): New key added to state, generation, that contains LLM generation
print("---GENERATE---")
question = state["question"]
documents = state["documents"]
generation = rag_chain.invoke({"context": documents, "question": question})
return {"documents": documents, "question": question, "generation": generation}

def grade_documents(state): # node. Determines whether the retrieved documents are relevant to the question If any document is not relevant, we will set a flag to run web search
# Args: state (dict): The current graph state
# Returns: state (dict): Filtered out irrelevant documents and updated web_search state
print("---CHECK DOCUMENT RELEVANCE TO QUESTION---")
question = state["question"]
documents = state["documents"]
filtered_docs = [] # Score each doc
web_search = "No"
for d in documents:
score = retrieval_grader.invoke({"question": question, "document": d.page_content})
grade = score['score']
if grade.lower() == "yes": # Document relevant
print("---GRADE: DOCUMENT RELEVANT---")
filtered_docs.append(d)
else: # Document not relevant
print("---GRADE: DOCUMENT NOT RELEVANT---")
# We do not include the document in filtered_docs. We set a flag to indicate that we want to run web search
web_search = "Yes"
continue
return {"documents": filtered_docs, "question": question, "web_search": web_search}
def web_search(state): # Web search based based on the question 
# Args: state (dict): The current graph state
# Returns: state (dict): Appended web results to documents
print("---WEB SEARCH---")
question = state["question"]
documents = state["documents"]

docs = web_search_tool.invoke({"query": question}) # Web search
web_results = "\n".join([d["content"] for d in docs])
web_results = Document(page_content=web_results)
if documents is not None:
documents.append(web_results)
else:
documents = [web_results]
return {"documents": documents, "question": question}

def route_question(state): # Conditional edge. Route question to web search or RAG.
# Args: state (dict): The current graph state
# Returns: str: Next node to call
print("---ROUTE QUESTION---")
question = state["question"]
print(question)

source = question_router.invoke({"question": question})  
print(source)
print(source['datasource'])
if source['datasource'] == 'web_search':
print("---ROUTE QUESTION TO WEB SEARCH---")
return "websearch"
elif source['datasource'] == 'vectorstore':
print("---ROUTE QUESTION TO RAG---")
return "vectorstore"

def decide_to_generate(state): # Determines whether to generate an answer, or add web search
# Args: state (dict): The current graph state
# Returns: str: Binary decision for next node to call
print("---ASSESS GRADED DOCUMENTS---")
question = state["question"]
web_search = state["web_search"]
filtered_documents = state["documents"]

if web_search == "Yes": # All documents have been filtered check_relevance. We will re-generate a new query
print("---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---")
return "websearch"
else: # We have relevant documents, so generate answer
print("---DECISION: GENERATE---")
return "generate"

def grade_generation_v_documents_and_question(state): # Conditional edge. Determines whether the generation is grounded in the document and answers question.
# Args: state (dict): The current graph state
# Returns: str: Decision for next node to call
print("---CHECK HALLUCINATIONS---")
question = state["question"]
documents = state["documents"]
generation = state["generation"]

score = hallucination_grader.invoke({"documents": documents, "generation": generation})
grade = score['score']

if grade == "yes": # Check hallucination
print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---")
print("---GRADE GENERATION vs QUESTION---") # Check question-answering
score = answer_grader.invoke({"question": question,"generation": generation})
grade = score['score']
if grade == "yes":
print("---DECISION: GENERATION ADDRESSES QUESTION---")
return "useful"
else:
print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---")
return "not useful"
else:
pprint("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---")
return "not supported"

workflow = StateGraph(GraphState)

# Define the nodes
workflow.add_node("websearch", web_search) # web search
workflow.add_node("retrieve", retrieve) # retrieve
workflow.add_node("grade_documents", grade_documents) # grade documents
workflow.add_node("generate", generate) # generatae

# Build graph and compile
workflow.set_conditional_entry_point(
route_question,
{
"websearch": "websearch",
"vectorstore": "retrieve",
},
)

workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
"grade_documents",
decide_to_generate,
{
"websearch": "websearch",
"generate": "generate",
},
)
workflow.add_edge("websearch", "generate")
workflow.add_conditional_edges(
"generate",
grade_generation_v_documents_and_question,
{
"not supported": "generate",
"useful": END,
"not useful": "websearch",
},
)

app = workflow.compile()

# Test the graph
from pprint import pprint
inputs = {"question": "What are the types of agent memory?"}
for output in app.stream(inputs, {"recursion_limit": 3}):
for key, value in output.items():
pprint(f"Finished running: {key}:")
pprint(value["generation"])

다음과 같이 출력되면 성공한 것이다..

이외에, Langgrpah를 사용하면, 다음과 같은 작업을 에이전트가 수행할 수 있다.

레퍼런스

2024년 6월 27일 목요일

ROS2 로봇 패키지 프로그램 개발 방법 정리

이 글은 ROS2 로봇 패키지 프로그램 개발 방법 정리한다.

URDF 모델링

시뮬레이션을 위한 Universal Robot Description Format (URDF) 을 사용해 모델링한다. URDF는 XML포맷으로 로봇을 물리적으로 정의한다. 이 모델은 주요 두 컴포넌트가 있다. 
  • LINKS: 강체(RIGID PIECE)를 정의한다. 링크는 뼈와 같은 역할을 하며 각 조인트로 연결된다.
  • JOINTS: 조인트는 연결된 링크 간에 운동이 가능하게 한다. 조인트는 서보 모터 등으로 구현된다.
회전 조인트는 라이다와 같이 회전 운동을 한다. 라이다를 가진 로버를 모델링을 위해, 터미널에서 다음을 설치한다.
sudo apt install ros-humble-joint-state-publisher-gui
sudo apt install ros-humble-xacro

이동형 로버 모델링
연구를 위해, 간단한 이동형 로버를 모델링한다. 다음 명령을 실행한다.
cd ~/projects/ros2/src
ros2 pkg create --build-type ament_cmake basic_mobile_robot
mkdir config launch maps meshes models params rviz worlds
cd ~/projects/ros2
colcon build

이제 다음과 유사한 로버 모델을 만든다. 

이 모델은 두개의 바퀴(revolute joints), 앞 바퀴(caster wheel), IMU, GPS를 가진다.  
cd ~/projects/ros2/src/basic_mobile_robot/models
subl basic_mobile_bot_v1.urdf

다음 코드를 입력한다. 
<?xml version="1.0" ?>
<robot name="basic_mobile_bot" xmlns:xacro="http://ros.org/wiki/xacro">

  <!-- ****************** ROBOT CONSTANTS *******************************  -->
  <!-- Define the size of the robot's main chassis in meters -->
  <xacro:property name="base_width" value="0.39"/>
  <xacro:property name="base_length" value="0.70"/>
  <xacro:property name="base_height" value="0.20"/>
	
  <!-- Define the shape of the robot's two back wheels in meters -->
  <xacro:property name="wheel_radius" value="0.14"/>
  <xacro:property name="wheel_width" value="0.06"/>

  <!-- x-axis points forward, y-axis points to left, z-axis points upwards -->
  <!-- Define the gap between the wheel and chassis along y-axis in meters -->
  <xacro:property name="wheel_ygap" value="0.035"/>

  <!-- Position the wheels along the z-axis -->
  <xacro:property name="wheel_zoff" value="0.05"/>

  <!-- Position the wheels along the x-axis -->
  <xacro:property name="wheel_xoff" value="0.221"/>

  <!-- Position the caster wheel along the x-axis -->
  <xacro:property name="caster_xoff" value="0.217"/>

  <!-- Define intertial property macros  -->
  <xacro:macro name="box_inertia" params="m w h d">
    <inertial>
      <origin xyz="0 0 0" rpy="${pi/2} 0 ${pi/2}"/>
      <mass value="${m}"/>
      <inertia ixx="${(m/12) * (h*h + d*d)}" ixy="0.0" ixz="0.0" iyy="${(m/12) * (w*w + d*d)}" iyz="0.0" izz="${(m/12) * (w*w + h*h)}"/>
    </inertial>
  </xacro:macro>

  <xacro:macro name="cylinder_inertia" params="m r h">
    <inertial>
      <origin xyz="0 0 0" rpy="${pi/2} 0 0" />
      <mass value="${m}"/>
      <inertia ixx="${(m/12) * (3*r*r + h*h)}" ixy = "0" ixz = "0" iyy="${(m/12) * (3*r*r + h*h)}" iyz = "0" izz="${(m/2) * (r*r)}"/>
    </inertial>
  </xacro:macro>

  <xacro:macro name="sphere_inertia" params="m r">
    <inertial>
      <mass value="${m}"/>
      <inertia ixx="${(2/5) * m * (r*r)}" ixy="0.0" ixz="0.0" iyy="${(2/5) * m * (r*r)}" iyz="0.0" izz="${(2/5) * m * (r*r)}"/>
    </inertial>
  </xacro:macro>

  <!-- ****************** ROBOT BASE FOOTPRINT ***************************  -->
  <!-- Define the center of the main robot chassis projected on the ground -->	
  <link name="base_footprint"/>

  <!-- The base footprint of the robot is located underneath the chassis -->
  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link" />
    <origin xyz="0.0 0.0 ${(wheel_radius+wheel_zoff)}" rpy="0 0 0"/>
  </joint>

  <!-- ********************** ROBOT BASE *********************************  -->
  <link name="base_link">
    <visual>
      <origin xyz="0 0 -0.05" rpy="1.5707963267949 0 3.141592654"/>
      <geometry>
        <mesh filename="package://basic_mobile_robot/meshes/robot_base.stl" />
      </geometry>
      <material name="Red">
        <color rgba="${255/255} ${0/255} ${0/255} 1.0"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>
    </collision>

    <xacro:box_inertia m="40.0" w="${base_width}" d="${base_length}" h="${base_height}"/>
    
  </link>

  <gazebo reference="base_link">
    <material>Gazebo/Red</material>
  </gazebo>

  <!-- *********************** DRIVE WHEELS ******************************  -->

  <xacro:macro name="wheel" params="prefix x_reflect y_reflect">
    <link name="${prefix}_link">
      <visual>
        <origin xyz="0 0 0" rpy="1.5707963267949 0 0"/>
        <geometry>
            <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
        <material name="White">
          <color rgba="${255/255} ${255/255} ${255/255} 1.0"/>
        </material>
      </visual>
      
      <collision>
        <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
      </collision>
      
      <xacro:cylinder_inertia m="110.5" r="${wheel_radius}" h="${wheel_width}"/>
      
    </link>

    <!-- Connect the wheels to the base_link at the appropriate location, and 
         define a continuous joint to allow the wheels to freely rotate about
         an axis -->
    <joint name="${prefix}_joint" type="revolute">
      <parent link="base_link"/>
      <child link="${prefix}_link"/>
      <origin xyz="${x_reflect*wheel_xoff} ${y_reflect*(base_width/2+wheel_ygap)} ${-wheel_zoff}" rpy="0 0 0"/>
      <limit upper="3.1415" lower="-3.1415" effort="30" velocity="5.0"/>
      <axis xyz="0 1 0"/>
    </joint>
  </xacro:macro>

  <!-- Instantiate two wheels using the macro we just made through the 
       xacro:wheel tags. We also define the parameters to have one wheel
       on both sides at the back of our robot (i.e. x_reflect=-1). -->
  <xacro:wheel prefix="drivewhl_l" x_reflect="-1" y_reflect="1" />
  <xacro:wheel prefix="drivewhl_r" x_reflect="-1" y_reflect="-1" />

  <!-- *********************** CASTER WHEEL ******************************  -->
  <!-- We add a caster wheel. It will be modeled as sphere.
       We define the wheel’s geometry, material and the joint to connect it to 
       base_link at the appropriate location. -->
  <link name="front_caster">
    <visual>
      <geometry>
        <sphere radius="${(wheel_radius+wheel_zoff-(base_height/2))}"/>
      </geometry>
      <material name="White">
        <color rgba="${255/255} ${255/255} ${255/255} 1.0"/>
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <geometry>
        <sphere radius="${(wheel_radius+wheel_zoff-(base_height/2))}"/>
      </geometry>
    </collision>
    <xacro:sphere_inertia m="10.05" r="${(wheel_radius+wheel_zoff-(base_height/2))}"/>
  </link>

  <gazebo reference="front_caster">
    <mu1>0.01</mu1>
    <mu2>0.01</mu2>
    <material>Gazebo/White</material>
  </gazebo>

  <joint name="caster_joint" type="fixed">
    <parent link="base_link"/>
    <child link="front_caster"/>
    <origin xyz="${caster_xoff} 0.0 ${-(base_height/2)}" rpy="0 0 0"/>
  </joint>
  
  <!-- *********************** IMU SETUP *********************************  -->
  <!-- Each sensor must be attached to a link.                              --> 
  
  <joint name="imu_joint" type="fixed">
    <parent link="base_link"/>
    <child link="imu_link"/>
    <origin xyz="-0.10 0 0.05" rpy="0 0 0"/>
  </joint>

  <link name="imu_link"/>

  <!-- *********************** GPS SETUP **********************************  -->
  <joint name="gps_joint" type="fixed">
    <parent link="base_link"/>
    <child link="gps_link"/>
    <origin xyz="0.10 0 0.05" rpy="0 0 0"/>
  </joint>

  <link name="gps_link"/>

</robot>

이제 보이는 형태를 표현할 메쉬 파일을 STL 형식으로 다음 폴더에 저장한다. 해당 메쉬파일은 여기(https://drive.google.com/drive/folders/1e9yB8f_LD7ty0M1h_mbd0kB8Hxr4TvfV) 에서 다운로드한다.
cd ~/projects/ros2/src/basic_mobile_robot/meshes

참고로, 이 메쉬 파일은 블렌더(blander)로 모델링된 것이다. 메쉬 저장 결과는 다음과 같다.

다음과 같이 명령 실행한다.
cd ~/projects/ros2/src/basic_mobile_robot
subl package.xml

이제 package.xml 파일을 다음과 같이 수정한다.
  <buildtool_depend>ament_cmake</buildtool_depend>
  <exec_depend>joint_state_publisher</exec_depend>
  <exec_depend>robot_state_publisher</exec_depend>
  <exec_depend>rviz</exec_depend>
  <exec_depend>xacro</exec_depend>

패키지 실행 파일(launch)을 생성한다. 
cd ~/projects/ros2/src/basic_mobile_robot/launch
subl basic_mobile_bot_v1.launch.py

실행 코드는 여기(https://drive.google.com/file/d/1pOgjTNuItNbn39nq0ztjxN0XBxj_v6H2/view)서 다운로드해 넣는다.
import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():

  # Set the path to different files and folders.
  pkg_share = FindPackageShare(package='basic_mobile_robot').find('basic_mobile_robot')
  default_launch_dir = os.path.join(pkg_share, 'launch')
  default_model_path = os.path.join(pkg_share, 'models/basic_mobile_bot_v1.urdf')
  robot_name_in_urdf = 'basic_mobile_bot'
  default_rviz_config_path = os.path.join(pkg_share, 'rviz/urdf_config.rviz')
  
  # Launch configuration variables specific to simulation
  gui = LaunchConfiguration('gui')
  model = LaunchConfiguration('model')
  rviz_config_file = LaunchConfiguration('rviz_config_file')
  use_robot_state_pub = LaunchConfiguration('use_robot_state_pub')
  use_rviz = LaunchConfiguration('use_rviz')
  use_sim_time = LaunchConfiguration('use_sim_time')

  # Declare the launch arguments  
  declare_model_path_cmd = DeclareLaunchArgument(
    name='model', 
    default_value=default_model_path, 
    description='Absolute path to robot urdf file')
    
  declare_rviz_config_file_cmd = DeclareLaunchArgument(
    name='rviz_config_file',
    default_value=default_rviz_config_path,
    description='Full path to the RVIZ config file to use')
    
  declare_use_joint_state_publisher_cmd = DeclareLaunchArgument(
    name='gui',
    default_value='True',
    description='Flag to enable joint_state_publisher_gui')
  
  declare_use_robot_state_pub_cmd = DeclareLaunchArgument(
    name='use_robot_state_pub',
    default_value='True',
    description='Whether to start the robot state publisher')

  declare_use_rviz_cmd = DeclareLaunchArgument(
    name='use_rviz',
    default_value='True',
    description='Whether to start RVIZ')
    
  declare_use_sim_time_cmd = DeclareLaunchArgument(
    name='use_sim_time',
    default_value='True',
    description='Use simulation (Gazebo) clock if true')
   
  # Specify the actions

  # Publish the joint state values for the non-fixed joints in the URDF file.
  start_joint_state_publisher_cmd = Node(
    condition=UnlessCondition(gui),
    package='joint_state_publisher',
    executable='joint_state_publisher',
    name='joint_state_publisher')

  # A GUI to manipulate the joint state values
  start_joint_state_publisher_gui_node = Node(
    condition=IfCondition(gui),
    package='joint_state_publisher_gui',
    executable='joint_state_publisher_gui',
    name='joint_state_publisher_gui')

  # Subscribe to the joint states of the robot, and publish the 3D pose of each link.
  start_robot_state_publisher_cmd = Node(
    condition=IfCondition(use_robot_state_pub),
    package='robot_state_publisher',
    executable='robot_state_publisher',
    parameters=[{'use_sim_time': use_sim_time, 
    'robot_description': Command(['xacro ', model])}],
    arguments=[default_model_path])

  # Launch RViz
  start_rviz_cmd = Node(
    condition=IfCondition(use_rviz),
    package='rviz2',
    executable='rviz2',
    name='rviz2',
    output='screen',
    arguments=['-d', rviz_config_file])
  
  # Create the launch description and populate
  ld = LaunchDescription()

  # Declare the launch options
  ld.add_action(declare_model_path_cmd)
  ld.add_action(declare_rviz_config_file_cmd)
  ld.add_action(declare_use_joint_state_publisher_cmd)
  ld.add_action(declare_use_robot_state_pub_cmd)  
  ld.add_action(declare_use_rviz_cmd) 
  ld.add_action(declare_use_sim_time_cmd)

  # Add any actions
  ld.add_action(start_joint_state_publisher_cmd)
  ld.add_action(start_joint_state_publisher_gui_node)
  ld.add_action(start_robot_state_publisher_cmd)
  ld.add_action(start_rviz_cmd)

  return ld

RViz를 이용해 모델을 확인할 것이다. 그러므로, RViz 설정 파일을 다음과 같이 만든다.
cd ~/projects/ros2/src/basic_mobile_robot/rviz
subl urdf_config.rviz

설정 파일 내용은 여기(https://drive.google.com/file/d/1YTjMqv9q12kP_JCPLgp03-WqjtJ6vyi7/view)에서 다운로드 한다. 상세 설정은 다음과 같다. 
Panels:
  - Class: rviz_common/Displays
    Help Height: 78
    Name: Displays
    Property Tree Widget:
      Expanded:
        - /Global Options1
        - /Status1
        - /RobotModel1/Links1
        - /TF1
      Splitter Ratio: 0.5
    Tree Height: 557
Visualization Manager:
  Class: ""
  Displays:
    - Alpha: 0.5
      Cell Size: 1
      Class: rviz_default_plugins/Grid
      Color: 160; 160; 164
      Enabled: true
      Name: Grid
    - Alpha: 0.6
      Class: rviz_default_plugins/RobotModel
      Description Topic:
        Depth: 5
        Durability Policy: Volatile
        History Policy: Keep Last
        Reliability Policy: Reliable
        Value: /robot_description
      Enabled: true
      Name: RobotModel
      Visual Enabled: true
    - Class: rviz_default_plugins/TF
      Enabled: true
      Name: TF
      Marker Scale: 0.3
      Show Arrows: true
      Show Axes: true
      Show Names: true
  Enabled: true
  Global Options:
    Background Color: 48; 48; 48
    Fixed Frame: base_link
    Frame Rate: 30
  Name: root
  Tools:
    - Class: rviz_default_plugins/Interact
      Hide Inactive Objects: true
    - Class: rviz_default_plugins/MoveCamera
    - Class: rviz_default_plugins/Select
    - Class: rviz_default_plugins/FocusCamera
    - Class: rviz_default_plugins/Measure
      Line color: 128; 128; 0
    - Class: rviz_default_plugins/SetInitialPose
      Topic:
        Depth: 5
        Durability Policy: Volatile
        History Policy: Keep Last
        Reliability Policy: Reliable
        Value: /initialpose
    - Class: rviz_default_plugins/PublishPoint
      Single click: true
      Topic:
        Depth: 5
        Durability Policy: Volatile
        History Policy: Keep Last
        Reliability Policy: Reliable
        Value: /clicked_point
    - Class: nav2_rviz_plugins/GoalTool
  Transformation:
    Current:
      Class: rviz_default_plugins/TF
  Value: true
  Views:
    Current:
      Class: rviz_default_plugins/Orbit
      Name: Current View
      Target Frame: <Fixed Frame>
      Value: Orbit (rviz)
    Saved: ~

빌드를 위한  cmake 파일을 설정한다.
cd ~/projects/ros2/src/basic_mobile_robot
subl CMakeLists.txt

다음 설정에서 적색 표시 부분을 추가한다.
cmake_minimum_required(VERSION 3.8)
project(basic_mobile_robot)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

install(
  DIRECTORY config launch maps meshes models params rviz src worlds
  DESTINATION share/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

빌드 및 실행
다음과 같이 빌드한다. 
cd ~/projects/ros2
colcon build

다음과 같이 src 밑의 특정 패키지만 colcon 빌드할 수도 있다.
colcon build --packages-select basic_mobile_robot
colcon build --symlink-install --packages-select basic_mobile_robot

다음과 같이 빌드 패키지 경로를 설정한다.
. install/setup.bash 

이제 새 터미널을 실행하고, 이 패키지를 실행해 본다. 
ros2 launch -s basic_mobile_robot basic_mobile_bot_v1.launch.py

rviz2를 실행한다.
rviz2 -d ./src/basic_mobile_robot/rviz/urdf_config.rviz 

다음과 같이 개발된 로버 모델이 보여진다. 

모델 좌표계를 확인하기 위해, 다음 명령을 실행한다. 
sudo apt install ros-humble-tf2-tools
ros2 run tf2_tools view_frames.py
evince frames.pdf

다음과 같이 각 링크와 조인트 좌표를 확인해 본다. 
ros2 run tf2_ros tf2_echo base_link front_caster

LLM 모델 파닝튜닝 도중 만난 삽질 기록

이 글은 LLM 모델 파닝튜닝 도중 만난 삽질을 기록한다. 이 상황은 GPU 24GB 내에서 3B 모델을 파인튜닝하는 과정에서 발생된 문제이다(GPU 거지). 각 모델에 따라 다양한 삽질이 있는 데 대표적인 것만 추려본다.

Google LLM model의 Huggingface google model 403 Client Error 문제 삽질
이 글은 403 Client Error: Forbidden for url: https://huggingface.co/google config.json 삽질을 기록한다. 

허깅페이스에서 구글 학습 모델을 다운로드 시도할 때 나오는 메시지인데, 이게 시도하는 상황에 따라 랜덤이라 문제이다. 
짜증(내 피 같은 시간을 낭비하다니T.T)

물론 모델 사용은 허가를 받고 진행한 것이다.

이 상황에서 물론 다음 질답과 같이 모든 것을 시도 해보았다. 각 답변을 보면 알겠지만, 다들 안되서 헤매고 있다.
  • https://huggingface.co/google/gemma-2b/discussions/28
  • https://huggingface.co/google/gemma-2b/discussions/11
  • https://huggingface.co/google/gemma-7b/discussions/31
  • https://huggingface.co/google/gemma-2b-it/discussions/31
  • https://huggingface.co/google/gemma-2b/discussions/25
  • https://github.com/huggingface/diffusers/issues/6223
구글 모델은 개발자 접근성에 문제가 있어 보인다. 

패지키 설치 의존성 에러
작은 리소스에서 Lora를 파인튜닝 어답터로 사용하다 보니 bitandbytes 등 패키지를 기본으로 사용하는 데, 의존성 에러 문제가 발생하였다(최근에는 문제가 많이 해결된 것으로 보임).

결론
세상에 공짜는 없는 듯.. 딥러닝 초창기 우분투, NVIDIA CUDA, 벽돌 PC 반복할 때 생각나네. 아직은 sLLM 개발이 초창기라 발생하는 문제인 듯 보인다. 

추신. 그래도, 라마, 파이는 잘 되는 것 같다.




ROS2, 가제보 설치 및 패키지 개발 테크트리

이 글은 ROS2 및 가제보 설치 테크트리를 정리한 것이다. 

좀 더 상세한 내용은 참고한 레퍼런스를 확인한다.

ROS2 준비
환경 설정
개발환경은 우분투 22.04이다. 윈도우 버전에서 설치해 보았으나, 너무 과도한 용량과 개인적으로 제일 싫어하는 라이센스 잔뜩 걸린 윈도우 버전 QT 패키지를 피해서 수동 설치해야 하는 문제가 있어, 우분투로 진행한다.  미리 vscode, sublime 도구가 설치되어 있다고 가정한다.

ROS2 버전은 HUBBLE을 사용한다.

설치
ROS2 설치는 터미널 실행 후 다음과 같이 진행한다. 상세한 설명은 여기(ROS2 설치 문서)를 참고한다.
sudo apt update && sudo apt install locales

sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8

apt-cache policy | grep universe

sudo apt update && sudo apt install curl gnupg lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

sudo apt update
sudo apt upgrade

sudo apt install ros-humble-desktop

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc

설치된 폴더는 다음과 같다.
/opt/ros/humble 아래에, 
/bin 실행파일
/cmake 빌드 설정
/include 헤더
/lib 라이브러리
/opt 기타 의존 패키지
/share 패키지 빌드 환경 설정

설치 테스트
설치가 잘 되었는 지 터미널 창 두개를 띄워 각 창의 명령을 다음과 같이 실행한다.
ros2 run demo_nodes_cpp talker
ros2 run demo_nodes_py listener

문제가 없다면, 제대로 설치된 것이다.

개발도구 설치
다음과 같이 설치한다.
sudo apt update && sudo apt install -y build-essential cmake git libbullet-dev python3-colcon-common-extensions python3-flake8 python3-pip python3-pytest-cov python3-rosdep python3-setuptools python3-vcstool wget
python3 -m pip install -U argcomplete flake8-blind-except flake8-builtins flake8-class-newline flake8-comprehensions flake8-deprecated flake8-docstrings flake8-import-order flake8-quotes pytest-repeat pytest-rerunfailures pytest
sudo apt install --no-install-recommends -y libasio-dev libtinyxml2-dev libcunit1-dev

SLAM, NAVIGATION 패키지 설치
다음과 같이 설치한다. 
sudo apt install ros-humble-gazebo-ros-pkgs
sudo apt install ros-humble-gazebo-ros2-control

sudo apt install ros-humble-cartographer
sudo apt install ros-humble-cartographer-ros

sudo apt install ros-humble-navigation2
sudo apt install ros-humble-nav2-bringup

sudo apt-get install ros-humble-moveit

sudo apt-get install pyqt5-dev-tools
python3 -m pip install --upgrade pip
python3 -m pip install -U catkin_pkg cryptography empy ifcfg lark-parser lxml netifaces numpy opencv-python pyparsing pyyaml setuptools rosdistro
python3 -m pip install -U pydot PyQt5

이외, 외부 패키지는 다음과 같이 설치할 수 있다.
sudo apt install ros-humble-teleop-swist-joy

혹은, 
cd ~/projects/ros2/src
git clone https://github.com/ros2/teleop_twist_joy.git
cd ..
colcon build --symlink-install --packages-select teleop_twist_joy

패키지 빌드, 기본 정보 확인 및 테스트
ROS2는 catkin_make 대신 colcon을 빌드 도구로 사용한다. 
ROS2는 복수 개발 폴더를 제공한다. 빌드 후 devel폴더에 넣는 ROS1은 패키지 의존도를 높여 에러 발생 확률을 높였다. ROS2는 devel 폴더는 없애고, 사용자가 명시적으로 colcon build --symlink-install 명령으로 개발된 패키지를 설치하도록 정책이 변경되었다. 

다음과 같이 터미널 명령을 실행한다.
mkdir -p ~/projects/ros2/src/
cd ~/projects/ros2/
colcon build --symlink-install

에러가 없다면, 성공한것이다. 폴더 구조는 다음과 같다.
/build  빌드 설정 파일
/install  빌드된 라이브러리, 실행 파일 
/log  로그
/src  사용자 패키지 폴더

다음과 같이, pkg create 명령으로 패키지를 하나 만든다.
ros2 pkg create my_first_ros_rclpy_pkg --build-type ament_python --dependencies rclpy std_msgs

다음과 같이 소스 내에 setup.py를 다음과 같이 편집한다.
'console_scripts': [
'helloworld_publisher = my_first_ros_rclpy_pkg.helloworld_publisher:main',
'helloworld_subscriber = my_first_ros_rclpy_pkg.helloworld_subscriber:main',
],

my_first_ros_rclpy_pkg 폴더의 my_first_ros_rclpy_pkg/helloworld_publisher.py 를 다음과 같이 편집한다.
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String

class HelloworldPublisher(Node):
    def __init__(self):
        super().__init__('helloworld_publisher')
        qos_profile = QoSProfile(depth=10)
        self.helloworld_publisher = self.create_publisher(String, 'helloworld', qos_profile)
        self.timer = self.create_timer(1, self.publish_helloworld_msg)
        self.count = 0

    def publish_helloworld_msg(self):
        msg = String()
        msg.data = 'Hello World: {0}'.format(self.count)
        self.helloworld_publisher.publish(msg)
        self.get_logger().info('Published message: {0}'.format(msg.data))
        self.count += 1


def main(args=None):
    rclpy.init(args=args)
    node = HelloworldPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

동일하게 helloworld_subscriber.py를 만들고, 다음과 같이 편집한다.
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String

class HelloworldSubscriber(Node):
    def __init__(self):
        super().__init__('Helloworld_subscriber')
        qos_profile = QoSProfile(depth=10)
        self.helloworld_subscriber = self.create_subscription(
            String,
            'helloworld',
            self.subscribe_topic_message,
            qos_profile)

    def subscribe_topic_message(self, msg):
        self.get_logger().info('Received message: {0}'.format(msg.data))


def main(args=None):
    rclpy.init(args=args)
    node = HelloworldSubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard Interrupt (SIGINT)')
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

다시 빌드해 본다.
colcon build --symlink-install

이제 각 터미널에서 다음 명령으로 패키지의 각 노드를 실행한다.
. install/local_setup.bash 
ros2 run my_first_ros_rclpy_pkg helloworld_subscriber 
ros2 run my_first_ros_rclpy_pkg helloworld_publisher 

다음과 같이 메시지 토픽을 주고 받으며 잘 동작하는 것을 확인할 수 있다.

이제, 설치된 패키지를 확인해 보자. 
ros2 pkg list

다음과 같이 터틀봇을 각 터미널에서 실행해 본다.
ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key

다음과 같이 현재 실행중인 노드 정보도 확인할 수 있다.
ros2 node list
ros2 topic list
ros2 service list
ros2 node info /turtlesim
rqt_graph

ROS2 패키지 노드는 서로 토픽, 서비스, 액션, 파라메터를 주고 받거나 공유한다. 패키지 개발 전에 미리 설계해야 하는 것이 좋다.

가제보 설치
가제보(GAZEBO)는 ROS 기반 시뮬레이션 도구로 유용하다. 다음과 같이 설치한다.
sudo apt-get -y install gazebo

ROS2 및 도구 실행
다음과 같이 ROS2와 도구를 실행한다.
ros2 daemon start
ros2 daemon status

gazebo --verbose

다음과 같이 실행되면 성공한 것이다.

이제, 가제보를 이용해, 물리 법칙에 따라, 설계한 모델을 테스트할 수 있다. 

URDF 모델 패키지 개발
URDF는 " Unified Robotics Description Format"을 나타낸다 . 이는 로봇의 형상 및 기타 속성을 지정하는 데 사용되는 파일 형식이다. URDF 모델을 지원하는 패키지 프로그램을 개발해 본다.

다음 명령을 실행해 기본 패키지를 설치한다.
sudo apt install ros-humble-joint-state-publisher
sudo apt install ros-humble-joint-state-publisher-gui
sudo apt install ros-humble-xacro

작업할 프로그램 패키지를 만든다.
ros2 pkg create --build-type ament_cmake urdf_test

생성된 폴더 안에 다음과 같이 추가 폴더를 생성한다.
cd urdf_test/
mkdir launch urdf

다음과 같이 URDF를 코딩한다.
cd urdf
gedit model.urdf
<?xml version="1.0"?>
<robot name="Robot1">
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="1" radius="0.4"/>
      </geometry>
    </visual>
  </link>
</robot>

다음과 같이 패키지 파일을 수정 추가한한다.
cd ..
gedit package.xml

  <exec_depend>joint_state_publisher</exec_depend>
  <exec_depend>robot_state_publisher</exec_depend>
  <exec_depend>rviz</exec_depend>

실행파일을 다음과 같이 생성한다.
cd launch
gedit display.launch.py

이 파이썬 파일의 코드는 다음과 같다.
import launch
from launch.substitutions import Command, LaunchConfiguration
import launch_ros
import os

def generate_launch_description():
    pkgPath = launch_ros.substitutions.FindPackageShare(package='urdf_test').find('urdf_test')
    urdfModelPath= os.path.join(pkgPath, 'urdf/model.urdf')
    
    with open(urdfModelPath,'r') as infp:
    robot_desc = infp.read()
    
    params = {'robot_description': robot_desc}
    
    robot_state_publisher_node =launch_ros.actions.Node(
    package='robot_state_publisher',
executable='robot_state_publisher',
    output='screen',
    parameters=[params])
    
    
    joint_state_publisher_node = launch_ros.actions.Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
        name='joint_state_publisher',
        parameters=[params],
        condition=launch.conditions.UnlessCondition(LaunchConfiguration('gui'))
    )
    joint_state_publisher_gui_node = launch_ros.actions.Node(
        package='joint_state_publisher_gui',
        executable='joint_state_publisher_gui',
        name='joint_state_publisher_gui',
        condition=launch.conditions.IfCondition(LaunchConfiguration('gui'))
    )
    
    rviz_node = launch_ros.actions.Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen'
    )

    return launch.LaunchDescription([
        launch.actions.DeclareLaunchArgument(name='gui', default_value='True',
                                            description='This is a flag for joint_state_publisher_gui'),
        launch.actions.DeclareLaunchArgument(name='model', default_value=urdfModelPath,
                                            description='Path to the urdf model file'),
        robot_state_publisher_node,
        joint_state_publisher_node,
        joint_state_publisher_gui_node,
        rviz_node
    ]) 

빌드를 위해 cmake 파일을 생성한다.
cd ..
gedit CMakeLists.txt

if(BUILD_TESTING) 문장 위에 다음과 같이 수정한다.
install(
  DIRECTORY launch urdf
  DESTINATION share/${PROJECT_NAME}
)

if(BUILD_TESTING)

빌드한다.
colcon build
source ~/projects/ros2/install/setup.bash

RVIZ를 시작한다.
ros2 launch urdf_test display.launch.py

RVIZ 실행 후 설정은 다음과 같도록 한다. 그럼, 모델링된 URDF를 볼 수 있다.

부록: 가제보와 연결해 시뮬레이션하는 방법
이 내용은 정보가 많지는 않다. 다만, gazebo_ros_pkgs libgazebo_ros_laser 와 같은 패키지가 있으며, 다음과 같이 시뮬레이션 관련 프로젝트가 진행 중에 있어, 여기서 일부 내용을 확인할 수 있다.

레퍼런스