모델 학습 후 성능 최적화를 위해 하이퍼파라미터 튜닝을 수행할 때가 있다. Optuna 사용 방법은 여러 문서에 기술되어 있어 참고할 수 있지만 파이토치에서 분산처리를 적용한 Multi-GPU 환경에서 Optuna를 함께 실행하는 방법에 대한 레퍼런스가 적어 약간의 삽질을 수행했기 때문에 추후 참고 목적으로 작성한다. 또한 파이토치로 분산처리 적용하여 Multi-GPU 학습을 수행하고자할 때 mp.spawn의 리턴값을 일반적으로는 받아오지 못하는 경우가 있어 이에 대한 내용도 덧붙이고자 한다.

 

전체 의사코드는 아래와 같고, 각 함수마다 간단히 서술한다.

import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler

def setup(rank, world_size):
    dist.init_process_group(backend='nccl', init_method="tcp://127.0.0.1:1234", rank=rank, world_size=world_size)

def cleanup():
    dist.destroy_process_group()

def run_tuning(tuning_fn, world_size, epochs, lr, optimizer_name):
    parent_conn, child_conn = mp.Pipe()
    mp.spawn(tuning_fn, args=(world_size, epochs, lr, optimizer_name, child_conn), nprocs=world_size, join=True)
    while parent_conn.poll():
        best_mAP = parent_conn.recv()
    return best_mAP
    
def tune_hyperparameter(rank, world_size, optimizer, scheduler, epochs, trial):
    setup(rank, world_size)
    torch.cuda.set_device(rank)

    model = Model().to(rank)
    model = DDP(model, device_ids=[rank])
    
    train_dataset = ''
    valid_dataset = ''
    train_sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank, shuffle=True)
    valid_sampler = DistributedSampler(valid_dataset, num_replicas=world_size, rank=rank, shuffle=False)
    train_loader = torch.utils.data.DataLoader()
    valid_loader = torch.utils.data.DataLoader()

    for epoch in range(epochs):
        train()
        mAP = valid()
        ...

    conn.send(best_mAP)
    trial.report(mAP, epoch)
    if trial.should_prune():
        raise optuna.TrialPruned()

    cleanup()
    
def objective(trial, world_size):
    epochs = trial.suggest_int("epoch", 10, 20)
    lr = trial.suggest_float("lr", low=1e-7, high=1e-2, log=True)
    optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "AdamW", "AdamP", "RAdam"])
    
    best_mAP = run_tuning(tune_hyperparameter, world_size, epochs, lr, optimizer_name)
    return best_mAP
    
    
if __name__ == "__main__":
	seed = 42
    ngpus_per_node = torch.cuda.device_count()
    world_size = ngpus_per_node

    func = lambda trial: objective(trial, world_size, world_size)

    study = optuna.create_study(direction="maximize", sampler=RandomSampler(seed=seed))
    study.optimize(func, n_trials=5)

    pruned_trials = study.get_trials(deepcopy=False, states=[TrialState.PRUNED])
    complete_trials = study.get_trials(deepcopy=False, states=[TrialState.COMPLETE])

    trial = study.best_trial.value

 

Optuna 사용 핵심은 objective 함수 작성이다. 함수 내에는 튜닝할 하이퍼파라미터의 종류와 그 값의 범위값을 작성해준다. 이후 학습 루틴을 수행 후 그 결과를 리턴값으로 전달해주는 것이 끝이다. 학습루틴을 objective 함수 내부에 정의해도 되나 분산학습을 함께 적용하기 위해 별도의 함수로 만들어 실행한다. 

import optuna

def objective(trial, world_size):
    epochs = trial.suggest_int("epoch", 10, 20)
    lr = trial.suggest_float("lr", low=1e-7, high=1e-2, log=True)
    optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "AdamW", "AdamP", "RAdam"])
    
    best_mAP = run_tuning(tune_hyperparameter, world_size, epochs, lr, optimizer_name)
    return best_mAP
    
if __name__ == "__main__":
	func = lambda trial: objective(trial, world_size)
    study = optuna.create_study()
    study.optimize(func, n_trials=5)

run_tuning 함수는 인자로 실제로 하이퍼파라미터 튜닝을 수행할 함수를 받는다. run_tuning 함수는 다음과 같이 구성되어 있다. 

def run_tuning(tuning_fn, world_size, epochs, lr, optimizer_name):
    parent_conn, child_conn = mp.Pipe()
    mp.spawn(tuning_fn, args=(world_size, epochs, lr, optimizer_name, child_conn), nprocs=world_size, join=True)
    while parent_conn.poll():
        best_mAP = parent_conn.recv()
    return best_mAP

run_tuning은 실제로 하이퍼파라미터 튜닝을 수행할 함수를 첫 번째 인자(tuning_fn)로 받는다. mp.spawn은 리턴값이 없으므로 학습 관련 결과에 대한 값을 받아올 수 없다. 만약 multi-gpu를 통한 분산처리의 리턴값을 받아올 필요가 없다면 mp.spawn() 한 줄만 작성해주어도 된다. 하지만 필요한 경우가 있다. 이를 해결하기 위해 mp.Pipe()를 사용한다. 학습 관련 결과를 child_conn을 이용해 parent_conn으로 전송하는 것이다. child_conn은 학습 함수인 tuning_fn의 인자로 들어가서 학습 관련 결과에 대해 parent_conn으로 전달한다. mp.spawn이 종료되면 parent_conn에서 poll() 메서드와 recv() 메서드를 통해 그 결과를 가져올 수 있다. 

 

아래 함수는 실제로 파이토치에서 Multi-GPU를 활용해 학습하는 루틴을 정의한 함수다. 

def tune_hyperparameter(rank, world_size, optimizer, scheduler, epochs, trial):
    setup(rank, world_size)
    torch.cuda.set_device(rank)

    model = Model().to(rank)
    model = DDP(model, device_ids=[rank])
    
    train_dataset = ...
    valid_dataset = ...
    train_sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank, shuffle=True)
    valid_sampler = DistributedSampler(valid_dataset, num_replicas=world_size, rank=rank, shuffle=False)
    train_loader = torch.utils.data.DataLoader(...)
    valid_loader = torch.utils.data.DataLoader(...)

    for epoch in range(epochs):
        train()
        mAP = valid()
        ...

    conn.send(max_best_mAP)
    trial.report(mAP, epoch)
    if trial.should_prune():
        raise optuna.TrialPruned()

    cleanup()

함수의 가장 처음과 끝은 setup()과 cleanup()으로 각각 Multi-GPU로 학습 가능하도록 초기화하고 학습 이후 환경 초기화를 수행하는 역할을 한다. 모델은 DDP로 래핑해주고 데이터셋도 DistributedSampler를 사용한 결과로 데이터로더를 만들어준다. 이후 학습을 수행하고 conn.send()를 통해 그 결과를 parent_conn()으로 전달한다. 

파이썬으로 데이터베이스 처리를 위해 pymysql을 사용해 MySQL 쿼리는 자주 다뤘지만 pymongo를 이용한 MongoDB 쿼리는 많이 다뤄보지 못했다. pymongo 쿼리문은 pipeline으로 만들어 실행하는 것이 깔끔하게 작성하고 실행할 수 있다 생각들었다. 작성하고보니 pipeline은 단 3줄 밖에 안되지만 레퍼런스가 적다고 느꼈고 익숙하지 않아 조금의 시간이 소요됐다. 추후에도 많이 사용할 코드 스니펫일 것 같아 기록.

 

import os
from pymongo import MongoClient
from operator import itemgetter

client = MongoClient(host=os.environ.get('MONGO_HOST'), 
                     port=int(os.environ.get('MONGO_PORT')), 
                     username=os.environ.get('MONGO_USERNAME'), 
                     password=os.environ.get('MONGO_PASSWORD'))
db = client[os.environ.get('MONGO_DB')]
collection = db['name']

pipeline = []
pipeline.append({'$match': {'col1': val1, 'col2': {'$gte', 1682899200}}})
pipeline.append({'$project': {'_id': True, 'col1': True, 'col2': True, 'col3': True}})
pipeline.append({'$limit': 100000})

unsorted_docs = collection.aggregate(pipeline)

unsorted_documents = []
unsorted_documents.extend(unsorted_docs)

documents = sorted(unsorted_documents, key=itemgetter('event_time'))

for document in documents:
	...

ROS2에서 통신을 구현하기 위해 사용하는 서비스, 액션, 메시지에서 사용하는 interfaces를 커스텀하여 정의해야 하는 경우가 있다. 이를 커스텀하기 위해 별도의 폴더를 만들고 빌드해주지 않으면 파이썬에서 라이브러리로 불러오거나 C++에서 include할 때 에러가 발생한다.

 

1. interfaces 패키지 생성

ros2 pkg create interfaces --buile-type ament_cmake interfaces
  • srv, action, msg를 담는 인터페이스 폴더 역시 하나의 패키지로 생성해주어야 한다.
  • 개발언어가 파이썬이라도 buile-type을 ament_cmake로 설정이 필요하다.

 

2. 커스텀 파일 생성

mkdir srv
cd srv
touch AddTwoInts.srv
  • action이면 *.action, msg면 *.msg로 만들어준다. 예시를 위해 임의로 srv를 커스텀 하며, 두 개의 입력값을 더해주고 반환하는 서비스를 구현한다고 가정한다.

 

3. srv 파일 사용자 기입

int64 a
int64 b
---
int64 sum
  • vim AddTwoInts.srv 명령이후 위 내용 기입

 

4. CMakeLists.txt 파일에 내용 추가

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
	"srv/AddTwoInts.srv"
)

 

 

5. package.xml 내용 추가

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_ruintime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>


이후 colcon build 수행시 interfaces 패키지도 함께 빌드

ROS에서 명령을 실행하는 방법은 두 가지로 run과 launch가 있다. run은 단일한 명령이고 launch는 run의 집합이다. 만약 두 개의 ros 명령을 수행하려면 터미널 두 개를 띄우거나 실행하려는 두 명령을 다 입력해주어야 한다. ROS를 다루다보면 여러 ROS 명령을 수행해야 하므로 효율성을 위해 사실상 launch를 실질적으로 더 많이 사용한다. 파이썬을 이용해 launch 파일을 만들고 gazebo와 rviz를 띄우는 일종의 hello world를 수행해보자.

 

이를 위해 가장 먼저 작업 디렉터리를 생성하고 작업 디렉터리로 들어간다.

mkdir -p test_ws/src
cd test_ws/src

 

ROS는 패키지 단위로 프로그래밍이 이뤄지므로 아래 ROS 명령을 통해 패키지를 생성한다.

ros2 pkg create gazebo_pkg --build-type ament_python

 

“gazebo_pkg”는 생성할 패키지 명이다. --buile-type은 사용할 빌드 시스템을 의미한다. ROS1에서는 catkin이 사용되었으나 ROS2에서는 catkin의 업그레이드 버전인 ament를 사용한다. 여기서 ament_python은 파이썬 전용 ament 빌드 시스템이다. 위 명령어를 수행하면 다음과 같이 패키지 폴더가 생성되며 폴더로 들어가면 아래와 같은 파일이 자동으로 생성된다.

 

 

목표는 시뮬레이션을 위한 gazebo와 시각화를 위한 rviz를 같이 띄우는 것이다. 이를 위해 launch 파일을 만들어야 하며 이러한 launch 파일이 담길 폴더를 ‘launch’로 생성해준다. 이는 ROS 개발에 있어 컨벤션으로 가급적 지켜주면 좋다. 이후 test.launch.py를 만들어 준다.

 

 

이후 gazebo와 rviz2를 실행하는 아래 코드를 test.launch.py 파일에 작성해준다

from launch import LaunchDescription
from launch.actions import ExecuteProcess

def generate_launch_description():
    
    return LaunchDescription([
        ExecuteProcess(
            cmd=["gazebo"], output="screen"
        ),

        ExecuteProcess(
            cmd=["ros2", "run", "rviz2", "rviz2"], output="screen"
        ),
    ])


코드를 살펴보자면 launch 파일이 실행되기 위해 반드시 generate_launch_description 함수를 만들어야 한다. 이후 LaunchDescription에 수행할 명령을 리스트로 담아주면 된다. LaunchDescription은 해당 launch 파일이 실행해야 할 목록을 기술하는 클래스다. 안을 살펴보면 ExecuteProcess 클래스가 두 개 있다. 사용법이 매우 간단한 형태로 수행할 명령을 cmd 인자에 입력해주고 output 인자를 통해 로그를 터미널에 출력해줄 수 있도록 screen을 입력한다.

 

cd ~/test_ws

 

source /opt/ros/humble/local_setup.bash

  • ROS2 환경을 현재 셸 세션에 로드하여 ROS2 실행에 필요한 실행파일, 라이브러리, 환경변수 등에 접근할 수 있도록 하는 명령어다.
  • 터미널이 새로 열릴 때 마다 새로운 세션이 생성되므로 매번 입력해주어야 한다. 귀찮다면 .bashrc에 해당 명령어를 입력해두면 터미널이 생성될 때 마다 자동으로 실행된다.
  • 참고로 setup.bash가 있고 local_setup.bash가 있다. 둘 간의 차이점은 전자는 전역적으로 설치된 패키지 설정이고 후자는 지역적으로 설치된 패키지에 대한 설정을 수행한다. setup.bash를 수행하면 전역적으로 수행되어 간편하지만 다른 패키지와 충돌(?)이 발생할 수 있으므로 가급적(?) local_setup.bash를 사용한다.

만약 위 명령을 수행하지 않고 launch 파일을 실행하면 다음과 같이 만들어주었던 패키지 폴더를 찾지 못했다는 에러를 확인할 수 있다.

 

 

위 명령을 수행하고 다시 실행해보자. 이번엔 다른 에러가 발생했다. gazebo_pkg 폴더 안에 test.launch.py가 없다고 한다.

 

이렇게 에러가 발생하는 이유는 빌드 당시 setup.py를 제대로 설정해주지 않아서 발생한다.

 

~/test_ws/src/gazebo_pkg/setup.py에 들어가서 아래 15번째 줄에 있는 os.path.join(’share’, package_name, ‘launch’), glob(’launch/*.launch.py’))를 추가해주자. (import os, from glob import glob도 함께)

 

 

 

이후 다시 colcon build --symlink-install --packages-select gazebo_pkg 명령을 통해 빌드를 다시 수행한 뒤, launch 파일 실행을 위해 ros2 launch gazebo_pkg test.launch.py를 수행해주면 아래와 같이 gazebo와 rviz2가 함께 실행되는 것을 확인할 수 있다.

 

 

만약 gazebo를 실행할 때 시뮬레이션 환경이 구성되어 있는 world 파일을 넣고자 한다면 test.launch.py와 setup.py를 아래와 같이 살짝만 코드를 바꿔 world 파일 경로 인자를 추가해주는 방식으로 gazebo 실행 시 이미 만들어진 시뮬레이션 환경을 구성할 수 있다. (world 파일이 없다면 이전 포스팅 참고)

 

import os
from glob import glob
from setuptools import setup

package_name = 'gazebo_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages', ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share/', package_name, 'launch'), glob('launch/*.launch.py')),
        (os.path.join('share/', package_name, 'worlds'), glob('worlds/*.world'))
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='roytravel',
    maintainer_email='roytravel@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
        ],
    },
)

참고로 리스트 data_files에 담긴 튜플 인자 두 개를 사용해 폴더와 파일을 복사한다. 두 번째 인자의 폴더와 파일을 복사해 첫 번째 인자에 있는 경로로 복사한다.

 

setup.py

import os
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    package_name = 'gazebo_pkg'
    world_filename = 'test.world'
    packge_path = os.path.join(get_package_share_directory(package_name))
    world_path = os.path.join(packge_path, "worlds", world_filename)
    
    return LaunchDescription([
        ExecuteProcess(
            cmd=["gazebo", world_path], output="screen"
        ),

        ExecuteProcess(
            cmd=["ros2", "run", "rviz2", "rviz2"], output="screen"
        ),
    ])

 

위와 같이 작성해주고 재빌드를 해준 뒤 launch 파일을 실행하면 gazebo를 실행할 때 직접 만들어 두었던 world 파일을 적용하여 gazebo를 실행시킬 수 있다.

 

 

 

Gazebo를 통해 시뮬레이션을 수행하기 위해선 시뮬레이션 환경구성에 필요한 모델을 추가해야 한다. 가령 자율주행이라면 모델은 차, 버스, 신호등, 횡단보도 등이 될 수 있다. 이러한 모델을 통해 구성한 시뮬레이션 환경은 반복해서 사용하기 때문에 Gazebo를 실행할 때 마다 이미 만들었던 모델을 불러올 수 있어야 한다. 이를 불러오기 위해서는 모델들을 담은 .world 파일이 필요하다. gazebo에서 모델을 만든 다음 .world 파일로 저장함으로써 생성할 수 있다.

 

gazebo 실행

gazebo --verbose


gazebo가 실행되면 위와 같이 아무 것도 없는 plane world가 나타난다. 이후 Insert 탭을 통해 모델을 추가하여 world를 구성할 수 있다.

 

 

로컬에 저장해둔 모델이 있다면 이를 사용할 수도 있지만 서버에서 제공해주는 모델을 사용해서 world를 구성할 수 있다. 맨 아래 http://models.gazebosim.org를 클릭해보면 해당 서버에서 제공해주는 모델 리스트를 아래와 같이 확인할 수 있다.

 

 

간단한 예시로 Bus와 SUV를 다음과 같이 불러올 수 있다.

 

 

world 파일로 저장하는 단축키인 Ctrl + Shift + S를 통해 저장할 수 있다. test.world 파일로 다음과 같이 저장하고 gazebo를 종료한다.

 

 

두 모델이 담긴 world 파일을 불러오기 위해 gazebo가 실행될 때 아래와 같이 명령어를 입력해주면 구성했던 시뮬레이션 환경을 다시 로딩할 수 있다.

 

gazebo test.world

 

 

1. 필요 패키지 설치

sudo apt-get install build-essential cmake # C/C++ 컴파일러 관련 라이브러리 및 도구
sudo apt-get install pkg-config # 컴파일 및 링크시 필요한 라이브러리 정보를 메타파일로부터 가져옴 
sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev # 이미지 파일 로드 및 저장
sudo apt-get install ffmpeg libavcodec-dev libavformat-dev libswscale-dev libxvidcore-dev libx264-dev libxine2-dev # 특정 코덱의 비디오 파일 읽기/쓰기
sudo apt-get install libv4l-dev v4l-utils # 실시간 웹캠 비디오 캡처를 위한 디바이스 드라이버 및 API
sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev # 비디오 스트리밍 라이브러리 설치 (Gstreamer)
sudo apt-get install libgtk-3-dev # opencv GUI (이외: libgtk2.0-dev, libqt4-dev, libqt5-dev)
sudo apt-get install libatlas-base-dev gfortran libeigen3-dev # OpenCV 최적화 라이브러리
sudo apt-get install python3-dev python3-numpy # OpenCV-Python 바인딩 & 행렬 연산
sudo apt-get install libfreetypes6-dev libharfbuzz-dev # opencv 한글 지원
sudo apt install unzip

 

2. OpenCV 소스코드 내려받기

mkdir ~/opencv && cd ~/opencv
git clone https://github.com/opencv/opencv.git
git clone https://github.com/opencv/opencv_contrib.git

 

3. CMake Setup

cd ./opencv
mkdir build && cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \\
-D CMAKE_INSTALL_PREFIX=/usr/local \\
-D WITH_TBB=OFF \\
-D WITH_IPP=OFF \\
-D WITH_1394=OFF \\
-D BUILD_WITH_DEBUG_INFO=OFF \\
-D BUILD_DOCS=OFF \\
-D BUILD_EXAMPLES=OFF \\
-D BUILD_TESTS=OFF \\
-D BUILD_PERF_TESTS=OFF \\
-D WITH_CUDA=ON \\
-D WITH_CUDNN=ON \\
-D OPENCV_DNN_CUDA=ON \\
-D CUDA_FAST_MATH=ON \\
-D CUDA_ARCH_BIN=7.5 \\    # 자신 GPU의 compute capability 값
-D WITH_CUBLAS=ON \\
-D WITH_CUFFT=ON \\
-D WITH_QT=ON \\
-D WITH_GTK=OFF \\
-D WITH_OPENGL=ON \\
-D WITH_V4L=ON \\
-D WITH_FFMPEG=ON \\
-D WITH_XINE=ON \\
-D BUILD_NEW_PYTHON_SUPPORT=ON \\
-D INSTALL_C_EXAMPLES=OFF \\
-D INSTALL_PYTHON_EXAMPLES=OFF \\
-D OPENCV_GENERATE_PKGCONFIG=ON \\
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \\
-D OPENCV_ENABLE_NONFREE=ON \\
-D BUILD_EXAMPLES=OFF ..
nproc # 자신 시스템의 코어 수 확인

# build (modify the core number '12' after option -j accordingly)
make -j8 # 자신 시스템 코어 수에 맞게 -j 다음의 숫자를 변경

# install
sudo make install

# check if it is installed successfully
pkg-config --modversion opencv4

 

4. 설치 확인

4.1 OpenCV 동작 여부 확인

아래 코드를 test.cpp라는 파일명으로 저장

#include "opencv2/opencv.hpp"
#include <iostream>  
  
using namespace cv;  
using namespace std;  
  
int main(int, char**)
{
    VideoCapture cap(0);
    if (!cap.isOpened()){
        printf("카메라를 열수 없습니다. \\n");
    }
  
    Mat frame;
    namedWindow("camera1", 1);
    for (;;) 
    {
        cap >> frame;
        imshow("camera1", frame);
        if (waitKey(20) >= 0) break;
    }
    return 0;  
}

아래 내용을 복사해 CMakeLists.txt로 저장

get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId} C CXX)

set (CMAKE_CXX_STANDARD 11)
cmake_minimum_required(VERSION 2.8)
find_package( OpenCV REQUIRED )

file(GLOB SOURCES  *.cpp)
 
add_executable(${PROJECT_NAME} ${SOURCES}  )
target_link_libraries( ${PROJECT_NAME} ${OpenCV_LIBS} )

CMake → Make → 실행

mkdir buld && cd build
cmake ../
make
./test

 

4.2 OpenCV GPU 사용 가능 여부 확인

아래 코드를 gpu.cpp라는 파일명으로 저장

#include <iostream>
using namespace std;

#include <opencv2/core.hpp>
using namespace cv;

#include <opencv2/cudaarithm.hpp>
using namespace cv::cuda;

int main()
{
    printShortCudaDeviceInfo(getDevice());

    int cuda_devices_numbers = getCudaEnabledDeviceCount();
    cout << "CUDA Device(s) Compatible: "  << cuda_devices_numbers << endl;
    DeviceInfo _deviceInfo;

    bool _isd_evice_compatible = _deviceInfo.isCompatible();
    cout << "CUDA Device(s) Compatible: " << _isd_evice_compatible << endl;

    return 0;
}

빌드

g++ -o gpu gpu.cpp $(pkg-config opencv4 --libs --cflags)

실행 결과에서 Number와 Compatible에서 모두 1이 나오면 GPU 동작 수행 가능

 

만약 1이 나오지 않는다면 설치과정 3의 옵션 중 아래 두 옵션의 수를 잘못 기입한 것임.

-D CUDA_ARCH_BIN=x.x

-D CUDA_ARCH_PTX=x.x

이를 해결하기 위해 해당 그래픽 카드에 맞는 버전으로 기입해줄 것

 

Reference

[1] https://darkpgmr.tistory.com/184

[2] https://webnautes.tistory.com/1767

[3] https://webnautes.tistory.com/1435

[4] https://mickael-k.tistory.com/211

1. PX4 소스코드 설치 및 Bash 스크립트 실행

git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd PX4-Autopilot
bash ./Tools/setup/ubuntu.sh

 

2. Gazebo 시뮬레이터 설치

sudo apt-get install gazebo

 

3. Gazeo SITL 시뮬레이터 실행

make px4_sitl gazebo
  • gazebo가 지원하는 기체 전체목록은 make list_config_targets 명령시 확인 가능
  • 만약 위 명령을 수행했을 때 에러가 발생한다면 sudo apt-get upgrade gazebo 명령 필요

 

4. 드론 이륙 및 착륙 명령 실행

pxh> commander takeoff
pxh > commander land

 

5. QGroundControl 설치

5.1 명령어 실행

sudo usermod -a -G dialout $USER
sudo apt-get remove modemmanager -y
sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y
sudo apt install libqt5gui5 -y
sudo apt install libfuse2 -y

 

5.2 QGC 바이너리 다운로드

 

5.3 QGC 바이너리 실행

chmod +x ./QGroundControl.AppImage
./QGroundControl.AppImage

 

 

PX4와 QGC가 mavlink로 연결됨을 확인: INFO [mavlink] partner IP: 127.0.0.1

 

 

6. QGC의 시작 좌표 변경

  • 아래 위치(석촌호수)를 .bashrc에 저장하여 터미널을 실행할 때 마다 설정되도록 내용 추가
export PX4_HOME_LAT=37.506700 #위도
export PX4_HOME_LON=127.097598 #경도
export PX4_HOME_ALT=15 # 고도

 

 

7. 시뮬레이션 속도 변경

  • 실제 시간 대비 시뮬레이션 속도 증감 가능 (아래는 2배)
export PX4_SIM_SPEED_FACTOR=2

 

 

8. GUI 없이 Gazebo 실행

  • 더 빠른 시뮬레이터 실행과 더 적은 리소스 사용
HEADLESS=1 make px4_sitl gazebo
  • 환경변수로 등록하고 싶을 경우 아래은 내용을 .bashrc에 추가
export HEADLESS=1

 

Reference

[1] https://docs.qgroundcontrol.com/master/ko/getting_started/download_and_install.html

[2] https://kwangpil.tistory.com/100

 

 

1. 필수 빌드 패키지 설치

sudo apt-get install build-essential

 

 

2. VSCode에서 C/C++ extension 설치

 

3. Ctrl + Shift + P로 구성 편집(UI) 선택

 

4. 컴파일러 선택 (C: gcc, C++: g++)

 

5. IntelliSense 모드

 

6. 설정파일 확인

- 위 설정한 값들이 아래 json 파일 형태로 저장됨을 확인

 

7. 템플릿에서 task.json 파일 만들기
- 터미널 → 작업 구성 → 템플릿에서 tasks.json 파일 만들기 → Others

 

8. 코드 복사 수정

- tasks.json에 아래 내용 복사 붙여넣기

{
    "version": "2.0.0",
    "runner": "terminal",
    "type": "shell",
    "echoCommand": true,
    "presentation" : { "reveal": "always" },
    "tasks": [
          //C++ 컴파일
          {
            "label": "save and compile for C++",
            "command": "g++",
            "args": [
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "group": "build",

            //컴파일시 에러를 편집기에 반영
            //참고:   https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher

            "problemMatcher": {
                "fileLocation": [
                    "relative",
                    "${workspaceRoot}"
                ],
                "pattern": {
                    // The regular expression.
                  //Example to match: helloWorld.c:5:3: warning: implicit declaration of function 'prinft'
                    "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning error):\\s+(.*)$",
                    "file": 1,
                    "line": 2,
                    "column": 3,
                    "severity": 4,
                    "message": 5
                }
            }
        },
        //C 컴파일
        {
            "label": "save and compile for C",
            "command": "gcc",
            "args": [
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "group": "build",

            //컴파일시 에러를 편집기에 반영
            //참고:   https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher

            "problemMatcher": {
                "fileLocation": [
                    "relative",
                    "${workspaceRoot}"
                ],
                "pattern": {
                    // The regular expression.
                  //Example to match: helloWorld.c:5:3: warning: implicit declaration of function 'prinft'
                    "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning error):\\s+(.*)$",
                    "file": 1,
                    "line": 2,
                    "column": 3,
                    "severity": 4,
                    "message": 5
                }
            }
        },
        // 바이너리 실행(Ubuntu)
        {
            "label": "execute",
            "command": "${fileDirname}/${fileBasenameNoExtension}",
            "group": "test"
        }
        // 바이너리 실행(Windows)
        // {
        //     "label": "execute",
        //     "command": "cmd",
        //     "group": "test",
        //     "args": [
        //         "/C", "${fileDirname}\\${fileBasenameNoExtension}"
        //     ]
   
        // }
    ]
}

 

9. 단축키 설정

- 파일 → 기본 설정 → 바로 가기 키 [Ctrl+K, Ctrl+S]

 

우측 상단 마우스 포인터가 가리키는 아이콘 클릭

 

빈 파일 확인

 

아래 내용 복사 붙여넣기

// 키 바인딩을 이 파일에 넣어서 기본값을 덮어씁니다.
[
    //컴파일
    { "key": "ctrl+alt+c", "command": "workbench.action.tasks.build" },
   
    //실행
    { "key": "ctrl+alt+r", "command": "workbench.action.tasks.test" }
]

 

 

Ctrl + Alt + C

 

save and compile for C++ 선택

 

Ctrl + Alt + R

 

 

이외에 ROS 개발 관련 VSCode Extension

  • ROS
  • URDF
  • XML Tools
  • YAML
  • MarkDown All in one
  • Highlight Trailing white spaces

상류란 무엇인가? 상류란 개인의 사익을 떠나 공동체의 이익을 위한 강한 사회적 책임의식을 가진자라고 말한다. 그렇다면 상류의 특징은 무엇일까? 후술하겠지만 책에서 상류가 보이는 여러 특징을 서술한다. 하지만 구체적으로 어떻게 상류가 될 수 있는지에 대한 이야기는 없다. 다만 맥락상 상류가 되기 위해선 상류의 사고의 기저를 이루는 철학과 사상을 깊이 이해할 필요가 있다는 한 대목을 볼 수 있었다. 어떻게 상류가 될 수 있는가에 대한 것은 사실 크게 궁금하지 않았다. 이미 철학과 사상이 중요하다는 견해를 전제하고 이에 대한 내용을 찾으려 했기 때문이다. 여러 차례 거듭 확인하고 싶었을 따름이다. 방법을 서술하지 않은 것은 어쩌면 상류가 되기 위한 방법을 서술하기에는 내용이 깊어지거나 서술한 책의 내용과의 동질성을 유지하기 어려울 수 있겠다는 생각이 들었다. 부족했던 내용 때문인지 이러한 내용을 채워보고자 하는 마음이 들었고 이에 대한 내용을 서술하고자 한다.
 
책에서 거듭 말하는 상류의 핵심은 강한 사회적 책임의식이다. 흔히 노블레스 오블리주라고 부르는 정신을 말한다. 노블레스 오블리주는 유교의 인(仁)을 떠오르게 한다. 인(仁)이란 인간이 하늘(天)로 부터 부여받은 자신의 본성을 의미한다. 인(仁)을 이해하게 되면 천지만물과 하나임을 알고 공동체를 위한 삶을 지향하게 된다. 인(仁)은 인간에 내재한 본성이기에 모두가 이에 대한 앎을 추구할 수 있고 또 이를 알아 군자가 되고 나아가 성인이 될 수 있다. 하지만 이는 쉽지 않다. 존재란 무엇이며 '나'는 어디서 유래했는가? '나'는 어디로 나아가고 있는가?와 같은 철학적 주제에 대한 천착이 필요하다. 이는 곧 자신의 삶의 근거와 목적과 의미를 깨닫는 것이다. 이러한 깨달음은 인(仁)을 직관할 때 이뤄진다. 이를 직관하는 시점이 개인이 사익을 추구하고자 하는 마음보다 사회 공동체의 이익을 추구하는 마음이 더 커지는 시점이다. 내안의 본성으로부터 사회 공동체를 위해 살아야 한다는 의무적인 '명령'을 받게 되는 시점이고 자율적으로 '복종'하게 되는 시점이다. 이것이 상류가 강한 사회적 책임의식을 가지게 되는 과정이다.
 
칸트도 일찍이 이러한 인(仁)에 대한 존재를 인식하고 인(仁)이 부과하는 의무에 대해 다음과 같이 예찬했다.
"""
의무여! 우리에게 복종을 요구하는 숭고하고도 위대한 이름이여! ..... 우리 의지를 움직이기 위해 우리 마음속에 들어온 자연 성향들을 쫓아내지 않으면서도 ..... 너에게 저항하는 그 모든 성향들을 침묵하게 만드는 너의 존귀함은 어디서 유래하는가? 자연적인 성향들과의 모든 유착을 늠름하게 거부하는 너의 고귀한 혈통은 어디서 시작되는가? 오직 인간만이 자신에게 부여할 수 있는 가치, 그러한 가치의 필수적 조건은 도대체 어느 근원에서 유래하고 있는가?
"""
 
서양철학사에서 가장 중요한 한 사람을 뽑으라면 칸트다. 모든 철학적 내용은 플라톤의 합리론과 아리스토텔레스의 경험론으로 귀결된다. 칸트는 이 합리론과 경험론의 끝없는 대립을 선험철학이라는 사상으로 종식시키고 통합시킨자다. 칸트의 선험철학의 핵심은 인(仁)의 또 다른 이름인 '이성'이다. 칸트는 '이성'을 통해 인(仁)인 자신의 본성을 자각한 자다. 이런 칸트가 기린 의무에 대한 예찬을 보면 숭고함, 존귀함, 고귀한 혈통이라는 말을 볼 수 있다. 의무를 부과하는 인(仁)의 성격(性格)을 이야기하는 것이다. 즉 인(仁)을 바라봤을 때 인(仁)은 숭고하며 존귀하고 고귀한 혈통과 같다는 것이다. 이러한 인(仁)의 뜻을 구현하며 살아가는 상류는 인(仁)의 성격을 상속받게 되는 것이고 이로 인해 고귀한 혈통 즉 귀족이라 불릴 수 있게 된다. 이러한 귀족적 정신의 발현을 우리는 노블레스 오블리주라 부른다.
 
이러한 귀족적 정신은 물질적 가치가 우선이 된 세계를 구가해서는 얻을 수 없다. 존재란 무엇인가에 대한 깊은 정신적 고찰이 필요하며 필연적으로 동반하는 정신적 고통을 감내해야 한다. 쉽게 얻은 것은 가치가 떨어진다고 하였던가 인고의 시간을 거쳐 얻은 귀족적 정신을 가진자가 죽더라도 그 정신은 살아남아 후대에 높은 가치로 계승된다.
 
--- 
아래는 책에서 좋았던 구절이다.
- 국가라는 공동체의 기풍이 어지러워 진것도 사회의 중심을 이뤄야할 상류들이 책임을 다하지 않았다는 증거다.
 
- 부와 권력은 차지했지만 황폐한 내면을 가진 이들의 횡포에 주눅들지 않는 품위가 필요하다.
 
- ... 말초적이기 짝이 없는 것들에 집착하며 산다.
 
- 그들이 영위하는 가치체계의 구조상 도덕과 양심의 순위는 돈에 대한 욕구보다 한참 하위에 머물 수 밖에 없다.
 
- 그들이 움켜쥐고 있는 돈과 권력을 떠나 벌거벗은 인간의 모습으로 그들을 바라볼 때 진정한 삶의 가치에 대한 어떤 명징성을 발견하게 될지도 모른다.
 
- 서구와 미국 상류의 겉모양을 닮으려는 데 혈안이 되어 있지, 정작 서양에서 수백년에 걸쳐 다져놓은, 진정한 상류들이 중요하게 생각하는 사상과 철학에는 관심이 없다는 것이다.
 
- 상류라는 개념은 문명의 지속성에 있어 가장 중요한 기저를 이룬다.
 
- 일상적 어휘의 차이가 무척이나 중요한 이유는 언어가 사람의 감성과 지성의 징표가 된다는 인식이 상류정신의 저변에 깔려있기 때문이다. 언어는 내면을 담아내고 있는 사고의 철학과 깊이를 말해준다.
 
- 책창에 어떤 책이 꽂혀있고 어떤 물건을 소중하게 여기는냐에 따라 두 사람은 전혀 다른 계급일 수 있다는 얘기다.
 
- 연봉이 억대라 할지라도 깨어 있는 시간은 노예인 사람들이 아닌가
 
- 돈을 숭배하는 것보다 사람을 천하게 만드는 것은 없다.
 
- 1908년 하버드 경영대학원 설립당시 총장이었던 A. 로런스 로웰은 경영대학원 설립 전제조건 중 하나가 사업가들이 '사익보다 더 숭고한 동기를 갖는 것'이라고 했다.
 
- 진정한 상류의 가장 중요한 특징은 사람이나 물건을 돈으로 평가하지 않는다는 점이다. 사람 품격의 고저는 돈을 얼마나 가지고 있느냐 하는 것보다 가진 돈을 어떻게 쓰느냐 하는것으로 가늠할 수 있다.
 
- 한 사회의 수준은 대다수 구성원이 어떤 가치를 상류적 가치로 여기는가 하는 것으로 가늠할 수 있다.
 
- 평균적으로 미국재벌의 사회적 책임의식 수준은 한국재벌에는 견주어 비교할 수 없을 정도로 높다.
 
- 상류 독자를 감동시키기 위해서는 그들의 지갑이 아니라 지성과 인격에 초점을 맞춰야 한다는 사실을 미국의 고급매체는 알고 있다.
 
- 사람의 내면에 고결함과 고상함과 숭고함, 즉 신성한 내면의 계급을 간직하고 산다는 것은 외형적으로 상류계급을 찬탈한자들에게서 주눅들지 않고 의연한 모습으로 살아가는 것이다. 

니체 철학에 관한 책이다. 니체 철학의 이해에 다가가기 위해서는 니체의 여러 저작을 두루 읽어야 한다고 들었다. 처음 니체를 접했던 책은 '차라투스트라는 이렇게 말했다'였고 여기서 니체가 말하고자 하는 초인에 대한 이해가 어려웠다. 니체 철학이 무언가 난해하다 느꼈기 때문이다. 플라톤의 합리론과 아리스토텔레스의 경험론으로 점철되어 왔던 서구 철학의 내용과 사뭇 다른 느낌이었고 나아가 서구 철학을 부정하는 느낌을 받았기 때문이다. 하지만 이 책을 통해 니체 철학을 이해하기 위한 방향에 대한 갈피를 잡을 수 있었다. 이유는 이 책이 니체의 여러 저작을 읽어보지 않더라도 니체가 저술한 여러 저작에서 핵심 사상이 드러난 부분만을 발췌하여 저자의 해설을 달아 두었기 때문이다. 

 

흔히 니체는 허무주의(니힐리즘)를 주장한 것으로 알려져 있다. 허무주의를 대표하는 말은 '신은 죽었다'일 것이다. '신'은 죽었다는 말에서 니체가 말하고자 하는 것은 인간들이 만들어 낸 '신'은 가상이라는 것이다. 인간이 자신 삶의 무의미와 무목적성을 직시하는 것이 두려워 의미와 목적을 부여하는 '신'을 만들었지만 이는 자기기만이며 거짓이고 허구라는 것이다. 그러니 이러한 인간 삶의 허무함을 직시할 것을 요구하는 것이 니체의 허무주의다. 하지만 니체는 허무주의를 주장하는 것에서 그치지 않았다. 이를 극복하는 방법을 제시했다. 인간 삶의 무의미와 무목적으로 인해 일어나는 고통과 허무함을 직시하고 생성변화하는 지상의 세계만이 유일한 실재임을 철저히 자각할 때 허무주의를 극복할 수 있다고 말이다.

 

이러한 허무주의를 극복하기 위해서 구체적으로 인간의 자기강화가 필요하다고 말하며 이를 위한 방법이 '힘에의 의지'라는 정신력을 고양시키는 것이라고 말했다. 이 '힘에의 의지'를 기르기 위해서는 우리가 살아가는 현실의 무상함과 고통을 긍정하며 이를 자기강화의 기회로 전환할 수 있어야 한다고 말한다. 니체는 이 '힘에의 의지'를 고양시키는 과정이 인간의 삶이라고 말하며 니체가 '차라투스트라'라는 표상으로 대신한 초인으로 나아가는 길이라는 것이다.

 

---

니체 철학을 올바르게 이해한게 맞다면, 정말로 니체가 말한 '신'은 허구인 것일까? 칸트의 순수이성과 기독교의 성령과 불교의 진아(眞我)와 힌두교의 아트만과 유교의 인(仁)과 도교의 도(道)는 무엇이란 말인가? 형이상학적 실재에 대한 직관의 경험은 거짓이고 스스로를 기만한 것일까? 니체가 말한 '초인'이란 이러한 형이상학적 실재라 여겨지는 것들과 다른 것이 맞는가? 니체가 옳다면 나는 존재의 무상함이라는 두려움을 떨치고 진정으로 허무주의를 직시할 수 있을까? 무엇이 맞을까? 나는 무엇을 모르고 있는가? 유신론과 무신론, 무엇이 옳은지 우리 인류는 끝으로 이에 대한 답을 '찾을' 수 있을까?

 

---

아래는 이 책을 읽으며 좋았던 구절이다.

 

- 다른 인간을 진정으로 돕는 행위는 그 인간에게 물질을 보태주는 게 아니라 그 사람이 진정한 자기를 발견하고 형성하도록 돕는 것이다.

 

- 인간에게 원숭이란 어떤 존재인가? 하나의 웃음거리 혹은 하나의 참기 어려운 수치가 아닌가? 그리고 초인에게는 인간 또한 그러한 존재이다. 하나의 웃음거리 혹은 참기 어려운 수치인 것이다.

 

- 힘에의 의지란 우리 내부에서 더 고귀하고 풍요로운 영혼을 갖도록 몰아대는 근원적인 힘이다.

 

- 본래의 자기가 되는 과정은 주관을 장악할 사명을 띤 '이념'이 밑바닥에서 서서히 성장한다. 그리고 그것은 명령하기 시작한다.

 

- 우리가 진정으로 신봉할 이념과 소명은 무수한 우회로와 경험을 거쳐서 서서히 내면 깊숙한 데서부터 성장해온다. 그러한 이념과 소망만이 깊이와 무게를 갖는다.

 

- 니체가 말하는 힘에의 의지는 통일성을 부여하는 것이다.

 

- 니체에게 자유란 자신이 설정한 위대한 과제와 이념에 자신의 본능과 욕구의 에너지를 집중시킬 수 있는 능력이다. 인간이 최대의 힘을 갖기 위해서는 기꺼이 하나의 방향이나 규칙에 자신을 구속하고 복종할 수 있어야 한다.

 

- 하나의 정신이 얼마만큼 진리에 견디는가? 얼마만큼 진리에 감히 부딪히는가? 나로서는 이것이 정신의 품격을 평가할 수 있는 기준이다.

 

- 인간도 자신의 성장을 위해 필요하다면 그동안 고수해온 세계관을 과감히 버릴 줄 알아야 한다.

 

- 인간이 자기극복을 자신의 과제로서 적극적으로 수용할 때 그는 어떤 곤경도 기꺼이 받아들인다.

 

- 긍지에 찬 인간은 자신은 전혀 또는 거의 남의 도움을 청하지 않으면서도 기꺼이 다른 사람들을 돕는다.

 

- 초인의 이념을 스스로 실현하기 위해 자기초극을 강행하는 것이야 말로 진정한 자기애임과 동시에 참된 인류애라고 할 수 있다.

 

- 창조한자들보다 더 훌륭한 '한 사람'을 창조하려는 두 사람의 의지, 이것을 나는 결혼이라 부른다.

 

- 플라톤적 전통 형이상학, 기독교, 이념들은 인간들이 자신들의 삶에 방향과 힘을 부여하기 위해 만들어낸 허구에 지나지 않는다.

 

- 진정한 교육이란 인간들로 하여금 자신의 본래적 자기를 구현하도록 돕는 것이다.

 

- 선이란 힘에의 의지를 고양하는데 기여하는 것이다.

 

- 도덕이란 공동체가 자신의 생존과 강화를 위해 정립한 가치체계일 뿐이다.

 

- 니체가 말하는 힘에의 의지란 위대해지고 싶은 욕망, 숭고해지고 싶은 욕망이다.

 

- 위계 질서의 제거는 문명의 몰락을 가져온다.

 

- 인간은 자신을 헌신할 수 있는 존재이유와 의미를 찾는다. 인간은 숭고한 의미와 이념을 위해 죽음도 불사할 수 있는 반면 자신을 바칠 수 있는 어떠한 의미도 이념도 없을 경우 공허감과 권태감에 시달린다.

+ Recent posts