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. 로런스 로웰은 경영대학원 설립 전제조건 중 하나가 사업가들이 '사익보다 더 숭고한 동기를 갖는 것'이라고 했다.
 
- 진정한 상류의 가장 중요한 특징은 사람이나 물건을 돈으로 평가하지 않는다는 점이다. 사람 품격의 고저는 돈을 얼마나 가지고 있느냐 하는 것보다 가진 돈을 어떻게 쓰느냐 하는것으로 가늠할 수 있다.
 
- 한 사회의 수준은 대다수 구성원이 어떤 가치를 상류적 가치로 여기는가 하는 것으로 가늠할 수 있다.
 
- 평균적으로 미국재벌의 사회적 책임의식 수준은 한국재벌에는 견주어 비교할 수 없을 정도로 높다.
 
- 상류 독자를 감동시키기 위해서는 그들의 지갑이 아니라 지성과 인격에 초점을 맞춰야 한다는 사실을 미국의 고급매체는 알고 있다.
 
- 사람의 내면에 고결함과 고상함과 숭고함, 즉 신성한 내면의 계급을 간직하고 산다는 것은 외형적으로 상류계급을 찬탈한자들에게서 주눅들지 않고 의연한 모습으로 살아가는 것이다. 

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

 

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

 

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

 

---

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

 

---

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

ROS2는 현재도 계속 개발 중으로 다양한 배포판이 나오고 있다. 이 중 Humble 배포판의 설치 내용을 정리한다.

 

윈도우로 설치하고자 한다면 아래와 같이 Microsoft Store에서 Ubuntu 22.04 LTS 버전을 다운로드 받고, 리눅스로 설치하고자 한다면 우분투 22.04에서 아래 나올 설치과정을 수행하면 된다.

 

1. ROS 설치

1.1 우분투 업데이트 및 언어 설정을 위한 locale 설치

sudo apt update && sudo apt install locales

 

1.2 locale 세팅

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

 

1.3 ROS2 설치를 위한 universe 저장소 활성화

apt-cache policy | grep universe

 

1.4 ROS2 apt 저장소를 시스템에 추가 & GPG 키 승인

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

 

1.5 ROS2 저장소를 sources.list에 추가

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

 

1.6 sources.list 업데이트에 따른 우분투 시스템 update 수행

sudo apt update
sudo apt upgrade

 

1.7 ROS2 설치 (다른 설치 과정에 비해 상대적으로 많은 시간 소요)

sudo apt install ros-humble-desktop

만약 풀버전을 다운로드 받고 싶다면 sudo apt install ros-humble-desktop-full 명령을 실행할 것. (차이점 파악 필요..)

 

1.8 ROS2 동작에 필요한 패키지 설치 (통신 라이브러리, CLI 도구 등 포함)

sudo apt install ros-humble-ros-base

 

2. ROS2 설치 검증

검증 1 - 통신 가능 여부

ROS2의 'Hello World'를 통해 정상적으로 설치되었는지를 확인한다. 이는 두 개의 터미널로 데이터를 주고 받는 것이 가능한지를 확인함으로써 가능하다. Talker와 Listener가 있고 Talker가 터미널에서 출력하는 메시지를 Listener가 받아서 그대로 출력할 수 있는지를 검증한다.

 

Talker 실행 (C++ 기반)

source /opt/ros/humble/setup.bash
ros2 run demo_nodes_cpp talker

 

Listener 실행 (Python 기반)

source /opt/ros/humble/setup.bash
ros2 run demo_nodes_py listener

 

(ROS2는 C++과 Python을 함께 지원한다.)

 

위 명령들을 실행하면 두 터미널에서 다음과 같은 형태로 Talker가 출력한 값을 Listener가 받아와 그대로 출력하는 것을 확인할 수 있다.

 

매번 source /opt/ros/humble/setup.bash를 입력하는 것이 귀찮으니 .bashrc 파일에 넣어 터미널이 실행될 때 자동으로 실행되도록 하자

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

 

검증 2 - 패키지 빌드 가능 여부

ROS2에서 패키지를 빌드하는 명령이 잘 수행되는지 여부를 판단하여 ROS2가 정상적으로 설치되었음을 확인한다.

soruce ~/opt/ros/humble/setup.bash
mkdir -p ~/robot/src/
cd ~/robot
colcon build --symlink-install

 

위 명령을 수행하면 다음과 같이 기존의 src 폴더와 더불어 build, install, log가 생성되며 이와 같이 생성된다면 ROS2가 정상적으로 설치된 것이다. 

아래는 동일한 내용으로 위와 같이 WSL이 아니라 Ubuntu 22.04를 설치하여 빌드 가능함을 확인하였다.

 

3. Gazebo 설치

Gazebo는 시뮬레이션 환경이다. 개발한 로봇 관련 알고리즘을 하드웨어에 적용시키기 이전에 소프트웨어적인 시뮬레이션을 통해 정상적으로 동작하는지 검증하기 위해 사용한다. 실제와 같이 중력, 기압, 고도, 풍속 등의 요소 등을 적용하여 시뮬레이션 할 수 있다. Gazebo이외에 MS에서 만든 Airsim이나 JMavSim도 있지만 상대적으로 레퍼런스가 많아 개발에 조금 더 용이한 Gazebo를 설치한다. 참고로 아래는 오픈소스 시뮬레이터 비교 표다. 

3.1 gazebo 설치

sudo apt-get -y install gazebo

(만약 gazebo 설치가 정상적으로 이뤄지지 않은 것 같다면 아래 명령 수행 - 차이점 파악 필요...)

sudo apt install ros-humble-gazebo-ros

 

위 명령을 통해 gazebo를 설치하고 난 뒤 다음과 같이 명령어를 수행하면 아래와 같이 Gazebo 시뮬레이터가 실행된다.

gazebo --verbose

 

 

만약 Failed to execute child process "dbus-launch" 에러가 발생하면 dbux-x11 패키지를 설치해줄 것

sudo apt install dbux-x11

 

4. ROS2 시각화 툴 설치 확인

ROS2에서 이뤄지는 통신 과정이나 시뮬레이션 상황을 시각화해서 보아야 할 때가 있다. rviz2가 시각화 도구다. ROS2 패키지 설치시 함께 설치된다. 아래와 같이 터미널에 rviz2를 실행하면 시각화 프로그램이 실행됨을 확인할 수 있다.

 

5. ROS2 개발 툴체인 설치

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

 

6. 기타 패키지 설치

# ROS2-gazebo 연동을 위한 패키지
sudo apt install ros-humble-gazebo-ros-pkgs
sudo apt install ros-humble-gazebo-ros2-control

# SLAM 관련 패키지 
sudo apt install ros-humble-cartographer
sudo apt install ros-humble-cartographer-ros

# Navigation 관련 패키지
sudo apt install ros-humble-navigation2
sudo apt install ros-humble-nav2-bringup

# ROS2 motion planning 프레임워크
sudo apt-get install ros-humble-moveit

# pyqt 설치
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

 

Reference

[1] https://makingrobot.tistory.com/159

[2] https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html

[3] https://omorobot.gitbook.io/manual/product/omo-r1mini/ros/ros2-foxy/ros2-ubuntu-20.04

[4] https://keep-steady.tistory.com/45

[5] https://puzzling-cashew-c4c.notion.site/ROS-2-Foxy-Windows-10-WSL-2-f50188cdf0c540119defa69fb40db221


더 나은 세상을 위해 살고 싶단 내 바람은 자본주의 시스템으로부터 더욱 심화된 빈곤이란 주제에 관심을 두게 했다. 나는 궁금했다. 사회적 빈곤은 정말로 해결할 수 없는 문제인가? 사회적 빈곤이 나타나는 구조적 원인은 무엇인가? 또 빈곤의 배태로부터 비롯되는 문제점과 지금 현 대응책은 무엇인가?

이 책은 이런 빈곤에 대한 거시적인 물음에 대한 답이라기보다 사회 문제에 대해 잘 알지 못하는 나 같은 사람에게도 쉽게 이해할 수 있도록 미시적 관점으로 가난의 한 모습을 비춰줌으로써 그 이해를 높일 수 있는 책이었다. 이 책은 재활용품을 수집하고 판매하는 노인들을 중점으로 이들이 처한 상황과 배경, 일상을 4년간 조사하고 연구한 내용을 바탕으로 쓰여졌다.

재활용품을 수집하고 판매하는 소위 가난한 사람들 대다수가 노인층으로 구성되어 있다. 그렇다면 이 노인층 중 재활용품을 수집하고 판매하는 사람들은 왜 가난하게 되었는가? 이러한 노인의 가난 원인은 여러가지가 있지만 가장 먼저 현재의 노인이 1930년 중반에서 1950년 중반에 태어났고, 1980년말 시행된 사회보험제도에서 제외된 처지라는 것이다. 때문에 안전망이 구비되고 노후를 준비할 수 있었던 이후 세대와 달리 자력갱생을 요구받는 상태이다. 또, 사회와 기술의 발전 때문이다. 이로 인해 전통적으로 지식창고 역할을 하던 노인이 책과 인터넷으로 대체되며 그 쓸모가 변했다는 것이다. 또 기초수급자 지정조건 때문이다. 이 조건은 '가족'의 소득과 재산이 일정 기준을 넘지 않아야 하지만 연락이 끊긴 자식의 경제수준이 기준 이상이기에 정부의 지원을 받을 수 없게 된다. 또 노인은 한국사회에서 임금노동 시장이나 공공근로 일자리에서 배제되어 있기 때문이다. 특히 여성 노인은 구직이 매우 어렵다. 가사도우미나 음식업 외에 많지가 않다. 때문에 결과적으로 돈을 벌 수 있는 유일한 방법인 재활용품 수거로 내몰리게 되는 것이다.

이 재활용품 수거라는 산업과 사회의 끝자락에 내몰린 일엔 노동의 그 어떤 정당성도 안정성도 바랄 수 없다. 하루종일 걷고 걸어 100kg~200kg 가량의 재활용품을 수집하더라도 겨우 단돈 8,000~9,000원도 안되는 수준의 대가를 받는다. 또 재활용품을 담기 위해선 리어카가 필요하다. 하지만 여성노인은 50kg가 나가는 리어카의 무게를 감당하기 힘들 뿐더러 재활용품이 더해진 무게는 견딜 수도 없다. 또 경쟁과 약탈이 있어 여성노인이 발견한 재활용품을 남성노인이 가로채는 경우도 있다. 이들은 신체경쟁에서 불리하며 쓰라린 패배감을 느끼게 된다. 또 용변을 보기 위해 세워둔 리어카나 카트를 도난당하기도 한다. 또 리어카를 끌다 주차차량과 부딪히면 수리비를 물어야 하고 과속방지턱을 넘다가 재활용품이 쏟아지기도 한다. 그리고 리어카를 끌기 위해 금속과 맞닿는 신체의 피부는 멍과 굳은 살로 덮힌다. 또 일부 건물주는 건물을 청소하는 대가로 재활용품을 가져갈 수 있도록 유사 고용을 하며 이에 따른 금전 대가를 지불하지 않는다.

이러한 궂은 상황에서 받는 돈은 한 끼 밥값이 채 되지 않는다. 일을 해도 가난을 벗어날 수 없는 처지인 것이다. 실제로도 우리 한국의 노인 고용율은 OECD 가입 국 중 가장 높다. 또 OECD 국가 중 노인의 상대적 빈곤율은 43.8%로 가장 높다. 즉 일을 많이 해도 빈곤하다는 것이다. 이러한 문제점을 해결하기 위해 내놓는 사회에서 이뤄지고 있는 현 대응책은 무엇이 있을까?

가장 먼저 노인일자리사업이다. 보건복지부에선 노인일자리 활동지원사업을 한다고 한다. 하지만 참여자 선발과정에서 치열한 경쟁이 필요하다. 또 참여자들은 월 평균 27만원씩 받았고 참여하지 않은 사람은 12만원씩 받지만 이 사업을 통해 노인의 상황이 실질적으로 나아졌다고 볼 수 없다. 이외에 사회적기업이나 협동조합에서 노인일자리 제공이다. 하지만 이러한 조직이 사업을 성장시키거나 지속성을 유지하는 경우가 매우 적다. 또 경로당을 공동작업장으로 바꾼 시도도 있다. 노인에게 쇼핑백 제조, 상품 포장 및 배달, 취약가구 무료 세탁, 먹거리 제조 등이다. 노인일자리와 부가가치 창출이란 긍정적인 측면도 있지만 여전히 참여하지 못하고 소외되는 이들이 존재한다.

저자는 정부가 이러한 노인일자리사업을 만들고 장려하지만 상황이 나아지지 않음을 짚는다. 즉 일자리의 질이 낮고, 또 낮을 수 밖에 없다고 말하며 사실상 재활용품 수집 노인의 일을 다른 것으로 전환시킬 방법은 없다고 말한다. 따라서 우리가 해야할 일은 노인들이 일을 하지 않고도 행복할 수 있는 방법을 마련하는 것이라 말하며 궁극적으로 노인들이 더 나은 기초소득을 가질 방법을 고민해야 한다고 말한다.

이 책을 통해 국가나 기업차원에서 이뤄지는 노인일자리사업으로도 불충분하다는 것을 여실히 알게 되었다. 사회적 빈곤이 사라진 세상은 가능할까? 지속적으로 변화를 거듭하는 인류에게 있어 새 문제가 발생하면 기존에 존재하는 이러한 빈곤과 같은 문제는 해결하기 어려울 수도 있을 것이다. 하지만 완전할 수 없다면 최소화시키는 노력은 할 수 있을 것이다. 이를 위해 국가 정책이나 지역 정책을 통해 저소득층을 지원하고 일자리를 창출하고, 주거, 의료, 교육의 기회를 주어 어려운 사람들에게 최소한의 삶의 질을 보장해야 한다. 현재로서의 효용성은 미비하나 그럼에도 이를 진전시키기 위한 우리의 걸음은 멈추지 않아야 할 것이다.

마음에 남는 대목

  • 이제는 가난의 문법이 바뀌었다. 도시의 가난이란 설비도 갖춰지지 않은 누추한 주거지나 길 위에서 잠드는 비루한 외양의 사람들로만 비춰지지 않는다.
  • 노인들의 삶이 순전히 개인의 잘못 때문에 생겨나는 걸까? 가난하고 싶어서 가난해진 사람은 없다.
  • 국가는 헌법에서 개인이 가지는 인권을 보장할 의무가 있음을 밝히고 있다.
  • 궁극적으로는 노인들이 재활용품을 줍지 않는 사회로 변화시켜 나가야 할 것이다.
  • 가난한 노인의 문제는 연민과 감동 그리고 자선 사업으로 해결 되지 않는다. 정작 필요한 건 안전한 자선활동이 아니라 현실에 대해 인식하고 실질적인 변화를 만드는 것이다.
  • 우리는 누군가의 가난을 보며 사회 체제의 불안정함과 미비함을 깨닫는 것처럼 보이지만, 그 깨달음은 사회를 바꾸어야 한다는 결론이 아니라 스스로의 상대적 안정감을 확신하고 불안정에 대한 두려움을 상기하는 것으로 이어질 따름이다.


2023년 첫 번째 독서다. 64권을 읽은 작년과 비교해 올해는 얼마나 읽게 될지 올해 말이 궁금해진다. 이 책은 맥도널드를 프렌차이즈 시스템으로 만들기 위해 도전했던 레이크록의 일화다. 종이컵을 파는 세일즈맨으로부터 시작해 맥도널드의 최고경영자까지 올라가는 과정에서 레이크록의 기업가 정신을 엿 볼 수 있어 좋았다. 다만 아쉬웠던 점은 이 책에서 강조하는 가치들은 지금에 이르러서 식상해진 가치가 아닌가라는 생각도 들었다. 이 책이 우리 인생의 바이블이라 했던 소프트뱅크 손정의 회장의 추천사가 있어 기대를 했지만 책이란 무릇 독자의 배경과 관심사에 따라 달리 느껴질 수 있음을 다시 한 번 느끼게 됐다. 아래는 책을 읽으며 밑줄 그었던 내용을 요약한 내용이다.

- 새 아이디어를 낼 때 처음부터 원대한 구상을 하지 않는다. 부분에서 전체로 나아갈 뿐이다.
- 단순해 보일 지언정 디테일의 중요성을 강조한다.
- 재능 있지만 성공하지 못한 사람이 많다. 이름값 못하는 천재도 많다. 또 세상엔 고학력의 낙오자로 가득하다. 전능의 힘을 가진 것은 끈기와 투지 뿐이다.
- 점포 입지와 점포 개발이 중요하다.
- 경쟁자를 이기려면 같은 입지에서 싸우는 것이 아니다. 자신만의 새 입지를 다져야 한다.
- "단, 비서님이 판단하지 말아주세요."
- 민주사회엔 미디어란 사회적 시스템이 필요하다. 잘못을 저지르는 개인과 회사에 경고를 보내기 위함이다. 아프지만 없다면 사회전체에 위기가 올 수 있다.
- "사장님 그건 틀렸습니다"라고 말할 수 있는 회사가 되어야 한다 - 야나이
- "전 창의력을 발휘하는 사람입니다. 회사에 돈을 벌어다 준단 말입니다. 다른 사람과 같은 취급 받을 생각없습니다" - 레이크록
- "나는 돈 보다도 진정한 참여의식을 느낄 수 있는 일자리를 찾고 있었다. 하지만 그런 일자리는 없었다." - 레이크록
- 나를 팔아야 물건도 팔 수 있다. 가장 먼저 팔아야 하는건 우리 자신이다.
- 위험 없는 성공 없다.
- 나는 문제에 압도되지 않는 법을 배웠다. 한 번에 한 가지 이상은 걱정하지 않았다. 문제가 있어도 불필요하게 조바심 내지 않으려 했다.
- 그녀의 그 강인함을 다감하고 따뜻한 성격이 감싸고 있었다. 좀처럼 조화를 이루기 힘든 덕목을 모두 가진 것이다.
- 모르는 게 있다면 도서관 책 모두를 뒤져서라도 알아내곤 했다.
- 매장을 오픈한 경험에 비추면 관건은 속도다.
- 그 사람의 능력을 믿지 못하면 애초 고용하지 말아야 한다.
- 돈 벌기 위해 돈 써라.
- 아무리 기민한 판단도 다른 사람에겐 독단으로 비칠 수 있다.
- 한 페이지짜리 제안서였지만 담긴 논리는 반박할 수 없었다. 광고효과, 장기적손해 등이 모두 기술 돼 있었음
- 정상의 자린 외롭다. 상실감만 남는다.
- 고객은 지불하는 비용에 걸맞는 서비스를 누려야 한다.
- 보통 재단을 세금 도피처라 생각한다.
- 일하는 즐거움, 일해야 하는 즐거움을 깨닫는 법을 배워야 한다.
- 미국 독립선언문에도 적혔듯 '우리'가 할 수 있는 최선은 행복을 추구할 자유를 주는 것이었다. 행복은 성취의 부산물이다.

문제 상황

Node JS에서 MySQL에 Update 쿼리를 날렸을 때 당최 쿼리가 반영되지 않았다. 나의 경우에는 HTML selectbox에서 A, B, C 값 중 하나를 선택하면 ajax를 이용해 MySQL에 업데이트하는 간단한 기능을 구현하고 싶었다. 하지만 업데이트 되지 않았고 결과를 출력해보니 아래와 같이 출력됐다.

 

OkPacket {
  fieldCount: 0,
  affectedRows: 0,
  insertId: 0,
  serverStatus: 2,
  warningCount: 0,
  message: '(Rows matched: 0  Changed: 0  Warnings: 0',
  protocol41: true,
  changedRows: 0
}

 

쿼리는 에러없이 실행되었지만 중요한 것은 Rows matched: 0 Changed: 0이라고 출력된 부분이다. 분명 쿼리 실행 이전, selectbox에서 값도 잘 받아왔고 쿼리문도 문제 없이 작성했지만 결과에 반영 되지 않았다. 직접 MySQL Workchbench로 값을 수정하면 반영이 잘되었고, 권한 문제도 없었기에 원인을 더욱 찾지 못해 아까운 시간 하루를 시간을 낭비했다.

 

에러 해결 방법

원인은 쿼리였다. 늘 사용했었고 다른 기능 구현에도 원활히 실행 됐었기에 문제가 없을 것이라 생각했다. 사용했던 쿼리는 다음과 같다.

const query = `UPDATE users SET column = ? WHERE username = ?`
return new Promise((resolve, reject) => {
	db.query(query, [value, username], (error, result) => {
    	if (error)
        	throw (error);
        else
        	reject(true);
    })
})

하지만 위와 같은 구문을 사용할 것이 아니라 아래와 같이 변경할 컬럼과 값, 조건문과 조건값을 별도의 변수에 담은 뒤 입력해주어야 한다.

const values = {'column': value};
const condition = {'username': username};
const query = `UPDATE users SET ? WHERE ?`;
return new Promise((resolve, reject) => {
    db.query(query, [values, condition], (error, result) =>{
        if (error)
            throw (error);
        else
            resolve(true);
    })
})

 

위와 같은 구문을 사용해 Update 문을 실행하면 아래와 같이 업데이트가 이루어지는 것을 확인할 수 있다.

 

 

애석하게도 원인의 본질적인 문제는 찾진 못했다. 다만 SQL Injection 방지를 위해 '?'를 사용해 쿼리를 실행하는 prepared statement와 관련이 있는 것으로 보인다. 구글에서 해결 방법을 찾기 어려워 혹시나 하는 마음으로 ChatGPT에게 질의한 결과로 얻은 해결방법이었다. 에러는 해결했지만 마음 한 구석에 세상의 발전에 대한 환희감과 동시에 인간의 지위에 대한 박탈감을 느꼈다. 단순히 이 문제에 대한 해답만이 아니라 여러 학문에 대한 답과 철학적인 질문들에도 막힘없는 대답을 들을 수 있었기 때문이다. ChatGPT가 열어갈 미래와 변화할 사회 모습이 궁금해지는 대목이다.

최근 많은 Redis 서버가 해킹당해 악성 봇넷으로 사용되고 있다고 한다. 그 이유는 Redis 서버에 인증없이 접속할 수 있다면 Redis 내부 명령어를 통해 악성코드를 실행할 수 있기 때문이다. 최근 Node JS로 개발 중인 서비스에서 캐시 DB로 Redis를 설치해 사용했다. 로컬에서 사용하니 실제로 서버 운영할 때 보안설정 하면되겠거니와 하며 인증없이 접속할 수 있도록 했었다. 하지만 로컬에 설치했던 Redis는 기본적으로 모든 네트워크와 연결 수립을 허용하는 것을 인지하지 못한 오판이었다. Redis는 기본 포트로 6379를 사용한다. 해커들은 6379 포트가 열린 IP를 브루트포싱하며 악성코드 실행을 위해 지속적으로 Redis 서버를 스캐닝 중에 있다. 다음과 같이 말이다.

 

 

redis-cli.exe를 통해 'monitor' 옵션을 설정해주면 내 Redis 서버를 스캐닝하는 외부 IP와 PORT를 확인할 수 있다. 또 만약 외부 IP에서 내 redis 서버에 임의의 key와 value를 설정하면 위 모니터링에 의해 값 설정 로그가 출력된다. 해커는 Redis 서버에 저장된 key-value를 탈취할 수 있지만, 휘발성격의 중요성이 떨어지는 데이터를 담는 Redis에선 key-value 탈취로 인한 피해보다 value에 악성페이로드를 담아 실행했을 때의 피해가 더 클 수 있다. 

 

위 info 로그는 "redis-cli -h <IP> -p 6379 info" 명령을 수행하면 발생하는 로그다. 실제로 실행해보면 아래와 같이 서버, 클라이언트, 메모리, CPU 등의 각종 정보를 조회할 수 있다. 세부적으로 운영체제 정보나 설정파일 경로를 획득할 수 있으니 해커 입장에서 이후에 페이로드 구성에 활용할 수 있다.

 

 

위 각종 정보를 살펴보던 도중 Clients 영역에서 connected_clients가 3인 것을 볼 수 있었다. 로컬에서 사용했는데 연결된 클라이언트가 존재할 수 있는지 궁금해졌다. client list라는 명령어가 있었고 이를 통해 살펴보니 모르는 외부 IP와 연결되어 있었다. (나머지 2개는 실행했었던 redis-cli monitor와 redis-cli client였다.)

 

IP 주소를 조회해보니 어디 상하이가 찍혀 나왔다. 사이트에 들어가보니 e-commerce 관련 회사 홈페이지였고 C&C 서버로 쓰이고 있는 것 같다. 

 

 

연결을 끊어줘야겠단 생각이 들었고 찾아보니 clinet kill 명령이 존재했다. 이를 통해 아래와 같이 중국 IP와의 연결을 끊어주었다.

 

 

이렇게 Redis 서버 보안에 주의를 돌리게 된 이유는 크게 두 가지다. 첫 번째는 지난밤 PC에서 비프음 보다 더 강렬한 소리가 났다. 이게 무슨 소리지?라고 어떻게 하면 이런 소리가 나지?라고 생각했다. 간단하게 윈도우 디펜더를 돌린 결과 아무 이상 없어 넘어갔다. 하지만 두 번째로 오늘 인증없이 접속 가능한 Redis 서버가 많고 많은 서버가 봇넷으로 사용되고 있음을 알게 됐다. 혹여 지난밤 소리와도 관련있지 않을까하여 Redis에 설정된 key 값과 운영체제에서 남기는 여러 로그를 분석해보았지만 다행히도 의심되는 흔적은 발견되지 않았다. 털리진 않았지만 이후 보안 설정도 개발과 병행해야겠단 생각으로 이어지게 됐다. 

 

따라서 보안 설정 방법을 알아보던 중 KISA가 발간한 클라우드 취약점 점검 가이드에 Redis 시스템 취약점 점검 가이드가 있었고, 가이드에서 말하는 4가지 보안 설정을 해주었다. 첫 번째는 Redis 인증 패스워드 설정이다. Redis config 파일을 살펴보면 SECURITY 영역에 'requirepass'가 있다. 기본값은 주석처리되어 있지만 주석을 해제하고 우측에 인증 패스워드 값을 설정해준다.

 

 

두 번째는 binding 설정이다. 기본적으로 Redis 서버는 모든 네트워크와 연결 될 수 있다. 만약에 bind가 주석처리 되어 있다면 말이다. bind의 기본 값이 주석처리 되어 있지만 이를 해제하고 로컬(특정 IP)에서만 접속할 수 있도록 한다. 

 

 

세 번째는 Slave 읽기 전용 모드 설정이다. redis는 master-slave 구조를 갖는다. master에 붙는 slave는 read-only 권한만을 가져서 master의 자원에 write할 수 없어야 한다. 하지만 이는 이미 아래와 같이 기본 설정으로 되어 있을 것이다. 그대로 두면 된다. 

 

 

네 번째는 rename-command 설정이다. Redis에서는 보안 설정을 목적으로 rename-command를 제공한다. 이는 명령어를 다른 이름으로 치환하는 역할을 한다. 가령 예를 들어 CONFIG 명령을 사용하기 위해서는 b840fc로 시작하는 해시와 같은 예측할 수 없는 값으로 설정할 수 있다. 또는 CONFIG ""을 통해 아예 CONFIG 명령을 사용할 수 없도록 만들 수 있다.

 

 

보안 설정 미흡으로 시스템이 털리는 것은 사소한 판단에서도 기인할 수 있음을 느끼게 됐다. 앞으론 서버 보안 설정에 주의를 더 기울일 수 있도록 해야겠다. 다음에 Redis 서버 설치한다면 위 4가지 설정을 제일 먼저 하는 걸로. (그나저나 최근 이따금씩 PC나 다른 장치에서 들리는 비프음 같은 것은 뭘까 싶다.)

 

(PS: requirepass 주석해제 및 값 설정으로 패스워드 설정이 안된다면 redis-cli에서 "config set requirepass <password>" 명령어로 설정할 수 있음)

 

Reference

[1] KISA 클라우드 취약점 점검 가이드

 

문제 상황

Node JS 다루다 보면 자주 마주치는 오류라고 한다. property가 없어서 발생한다고 하지만 분명 값이 있었기에 당최 그 이유를 알 수 없었다. 내가 이 오류를 마주한 구문은 try catch이었다. try와 catch 각각의 return 구문이 있었고, 신기하게도 try에서도 return되고 catch에서도 return 되는 기이한 현상이었다. 또 이와 더불어 console.log에 값이 두 개씩 찍힌다는 것도 문제였다. 하나는 올바른 값이 들어 있지만 하나는 값 없이 undefined만 출력됐다. 어디가 문제인지, 무엇이 문제인지, 무엇을 모르는지 올바르게 떠올리지 못해 거의 하루를 낭비했다.

 

나의 경우는 HTML 쪽이 문제였다. 정확히는 form에서 submit이 실행될 때 이벤트를 듣고 있는 addEventListener 함수가 문제였다. 생각지도 못했다. 아래와 같이 addEventListener 함수를 한 번 호출했지만 계속해서 두 번씩 호출되고 있었고 이 때문에 console.log 값이 두 번씩 찍혔다.

"use strict";

const name = document.querySelector("#name"),
    email = document.querySelector("#email"),
    submitBtn = document.querySelector("#submit");

submitBtn.addEventListener("click", findUsername);

 

해결 방법

addEventListener 함수가 두 번씩 호출되고 console.log가 두 번씩 출력되는 이유는 아래와 같이 HTML의 form에서 onsubmit="return false;"이 빠져있다면 addEventListener가 두 번씩 호출되기 때문이다. 정확히는 onsubmit이 form에서 submit이 실행되면 발생하는 이벤트 핸들러로 기본값이 true로 되어 있기 때문에 addEventLitener와 중복되어 두 번씩 실행되는 것이다. 

<form class="login-form" action="/find-username" method="post">
    <h4>이름과 이메일을 입력해주세요.</h4>
    <input id="name" type="name" placeholder="이름"/>
    <input id="email" type="email" placeholder="이메일"/><br><br>
    <input type="submit" id="submit" value="확인">

따라서 위와 달리 onsubmit="return false;"를 추가해주면 더 이상 console.log에서 두 번 출력되지 않고 값도 undefined로 출력되지 않는다. 

<form class="login-form" action="/find-username" method="post" onsubmit="return false;">
    <h4>이름과 이메일을 입력해주세요.</h4>
    <input id="name" type="name" placeholder="이름"/>
    <input id="email" type="email" placeholder="이메일"/><br><br>
    <input type="submit" id="submit" value="확인">

 

음성인식 앱 개발에 흥미가 있던 터라 이끌려 사게 된 책이다. 내용은 음성인식의 과거, 현재, 미래를 이야기 하며 음성인식이 가져올 산업구조의 변화에 대해 이야기한다. 이를 위해 현재 구글, 아마존, 애플, 마이크로소프트와 같은 글로벌 IT 기업에서 음성인식 관련해 어떤 행보를 보이고 있고 어떤 패권다툼이 있는지 이야기 한다. 평소 음성인식 앱 개발에 있어 이렇게 해야할 것 같다, 저렇게 해야할 것 같다고 생각했던 것들이 여러 IT 기업들에서 동일하게 고민한 바 있어 바른 방향으로 생각했구나 하는 작은 희열감을 느꼈고, 차마 생각이 닿지 못했던 부분들에 대해 알 수 있어 좋았다. 

 

1부: 경쟁

- 음성은 만능 리모컨화 되고 있고, AI 스피커는 집안 내 모든 가전제품을 제어하는 '허브'역할을 할 것이다. 

- AI 스피커는 음성이란 특성상 사무실보다 가정에 적합할 것이다.

- 음성의 검색결과는 구글과 달리 하나만 보게 될 것이고, 그 결과 검색엔진 상단의 콘텐츠 노출 경쟁이 심화될 것이다.

- 음성은 구글의 수십억달러짜리 광고 비즈니스 모델을 위협한다. 

- 클라우드 덕분에 마이크와 와이파이 칩만 있으면 어떤 디바이스도 음성인식 장치가 될 수 있다.

- 회사들은 사활을 걸고 지배적인 새 OS를 개발하기 위해 고군분투 중이다.

- AI에게 페르소나 부여가 필요하다. 페르소나가 있는 AI 스피커의 앱 사용율이 더 높다. (by Google)

- AI는 수십년간 차세대 거대시장으로 기대받았지만 안정적 이윤창출을 성공한적이 없다.

- 구글은 1,700개 이상의 액션을 보유 중이다.

 

2부: 혁신

- 세계 최초 챗봇은 엘리자다. 1960년대 중반 MIT에서 만들어졌고 심리치료사 역할을 했다. 이후 1972년 심리치료챗봇 패리(Parry)가 스탠퍼드 대학 정신과 의사에 의해 만들어졌다. 

- 대화 중 이어지는 다음 말이 올 수 있는 순열은 무한하다. 예상대로 대화를 흘러가도록 만들 수 없다. 

- 한 가지 주제에 집중한 챗봇은 그나마 만들 수 있다.

- 수학자 조지 불 이름을 딴 게 불(Bool) 논리다. 측량사 조지에베레스트 이름 딴 산이 에베레스트 산이다.

- 친밀감과 효율성은 Trade-off 적 성격을 띤다. 개인적 따듯함과 직업적 냉정함 사이의 균형이 필요하다.

- 구글의 AI 스피커 설계에 있어 핵심 원칙 중 하나는 사람처럼 말하되, 사람인 체 하지 않는 것이다.

- 희곡에선 강한 견해가 캐릭터를 흥미롭게 하는 요인이지만 AI 스피커 설계에 있어선 양극화를 일으키지 않을 정도의 성격을 만들어야 한다.

- 사용자들은 각각 고유한 AI 비서를 찾고 있다. 따라서 사용자 범주별 맞춤형 성격을 만들필요가 있다. 

- 각자 전문성을 갖춘 작은 모델로 나눠 앙상블을 구성했을 때 더 좋은 평점을 받았다. 

 

3부: 혁명

- 구글이 찾아주는 100만 개 링크는 시리의 정확한 답변 하나보다 훨씬 가치가 낮다.

- 원샷 답변이란 단 하나의 정확한 답변을 제공하는 것이다.

- 웹캠과 동작추적센서로 표정분석할 수 있다. 또 말 속도, 길이, 어조 분석을 통해 현재 상태에 대한 단서가 될 수 있다. 

- 자제력과 같은 것은 프로그래밍이 쉽지 않다. 또 시간 구분 감각에 대한 프로그래밍이 필요하다.

- 훌륭한 사람은 물론 강한신체와 운동능력을 겸비해야겠지만 고귀한 정신, 부드러운 마음, 고상한 영혼은 수 많은 미덕의 출발점이다.

- 거대 기술 기업이 어디로 향할지 미리 알아 보는 좋은 방법은, 그들의 특허 신청 서류를 검토해보면 된다.

논문 제목: Multi-Task Pre-Training for Plug-and-PlayTask-Oriented Dialogue System

게재 학회 / 게재 년도: ACL / 2022.05

 

이 논문은 기존의 모듈화된 dialogue system을 하나의 모델에서 end-to-end 방식으로 동작가능한 PPTOD라는 모델에 관한 논문이다. 기존의 dialogue system은 대개 4개의 모듈인 NLU, DST, DP, NLG로 나뉜 방식으로 구성된다. 논문에서는 이러한 모듈화된 방식에 순서성이 있다고 하여 cascaded 방식이라 부른다. 하지만 이 방식에는 크게 3가지 문제가 있다고 말하며 PPTOD 모델의 연구 배경을 말한다. 그 문제는 모듈화된 계단식 dialogue system 구성이 첫 번째로 이전 모듈에서 에러가 발생하면 이후 모듈에 에러가 전파된다는 것이며 두 번째로 모든 모듈에서 각 모델이 학습하기 위해 각각의 데이터셋 수집과 라벨링 과정이 필요하고 마지막으로 계단식이기에 필연적으로 Inference latency가 느려진다는 것이다.

따라서 이러한 문제점을 해결하기 위해 end-to-end 방식의 PPTOD라는 모델을 제시한다. 여기서 end-to-end란 기존의 NLU, DST, DP, NLG를 하나로 통합한 것이다. 따라서 유저의 utterance가 들어오면 한번에 1. 의도 분류 2. 슬롯 필링 3. 액션 생성 4. 대답 생성이 가능하다. 아래 PPTOD 모델을 만들기 위해 pre-training하는 예시를 살펴보자.

가장 앞에 "translate dialogue to A"는 하나의 모델에서 DST, NLU, DP, NLG 중 어떤 것인지 구분하기 위해 사용되는 일종의 prefix이다. 이 prefix는 위 그림과 같이 "belief state", "user intent", "dialogue act", "system response"가 있다. 두 번째로 DST를 위해 이전까지 유저와 봇이 주고 받은 대화와 현재 유저의 utterance를 모두 concatenation해준 다음 입력으로 넣어준다. 그렇게 되면 이제 prefix에 따라 생성되는 결과값이 4가지로 분리된다. 즉, slot filling된 결과, user intent, bot action, bot 응답이다. 간단히 정리하면 prefix와 유저 utterance가 입력으로 들어가고 4개의 결과를 모두 출력할 수 있는 구조인 것이다. 다르게 말하면 세부적으로 모듈화된 task-oriented dialogue 문제를 단일한 포맷의 text generation 문제로 바꾼 모델이다. 

이 T5 기반의 end-to-end 모델을 학습하기 위해 추가적인 pre-training과, fine-tuning 과정을 거쳤다. 데이터셋은 기존의 Dialogue System을 만들기 위해 공개된 아래 데이터셋을 조합했다.

총 80개 도메인의 230M개 가량의 유저 utterance 데이터셋을 사용했다. 우선 pre-training을 위해 사용한 세부적인 하이퍼파라미터는 learning rate: 5e-5, epoch: 10, optimizer: Adam, model: T5, max_seq_len: 1024, batch_size: 128, loss: maximum likelihood이며 구현의 용이성을 위해 허깅페이스 라이브러리를 사용했다. fine-tuning은 pre-training과 동일한 하이퍼 파라미터를 사용했다.

 

모델 학습에 사용하는 데이터셋의 형태는 $d = (z_t, x, y)$을 가진다. $d$는 데이터셋을 의미하고, $\displaystyle t \in \{NLU, DST, DP, NLG\}$이고 $z_t$는 "translate dialogue to A:" 형태의 prompt를 의미한다. $x$는 유저의 현재 발화 + 이전의 유저 발화 + 봇 응답이 전부 concatenation 된 형태이다. $y$는 생성해야할 target sequence를 의미한다. 학습에 사용한 loss 함수는 maximum likelihood를 사용했다. 

 

PPTOD 모델의 버전은 크게 3가지로 small, base, large 모델이 있다. 각각 t5-small, t5-base, t5-large에 대해 pre-training하고 fine-tuning한 결과이다. 다만 학습시킬 때 각각의 사이즈 별로 다른 configuration을 사용했다고 한다. 이에 대한 별도의 언급은 없다. t5 모델의 기본 configuration을 사용했을 것이다. 

 

PPTOD 모델을 평가하기 위해 크게 3가지 측면에서 벤치마킹을 수행했다. 1. end-to-end dialogue modeling 2. DST 3. user intent classification 측면이다. 벤치마킹을 위해 MultiWOZ 2.0과 MultiWOZ 2.1를 사용했다. 결과적으로 3가지 측면 모두 PPTOD 모델의 우수성을 이야기하고 있다. 

 

 

첨언하자면 MultiWOZ 데이터셋에 있는 Inform, Success, BLeU, Combined Score는 MultiWOZ 데이터셋에서 제시하는 평가 가이드라인이라고 한다. 또 Combined Score는 (Inform + Success) * 0.5 + BLUE로 계산된다고 한다. 위 표의 성능을 보면 PPTOD base 모델이 가장 좋다. PPTOD large 모델이 오히려 성능이 떨어지는 것은 사전 훈련 단계에서 보지 못했던 어휘에 대해 토큰을 생성하는 방법 학습할 때 능력이 떨어지는 것으로 분석한다고 말한다. 이 말이 잘 와닿진 않지만 일단은 PPTOD base 모델이 가장 좋다고 한다. 

 

이 논문의 저자들은 또 PPTOD 모델이 적은 데이터셋에서도 좋은 성능이 나는지 보기 위해서 학습 데이터셋을 1% 썼을 때, 5% 썼을 때, 10% 썼을 때, 20% 썼을 때에 대해 모델 성능을 비교했다. 참고로 표를 만들기 위해 5회 모델 학습에 대한 평균성능을 기재했다. 

1% 학습 데이터셋만으로도 다른 모델들보다 성능이 뛰어남을 보인다. 여기까지가 1. end-to-end dialogue modeling에 대한 벤치마크 평가다. 이외의 2. DST 측면의 평가와 3. user intent classification 측면의 평가도 모두 PPTOD large 모델이 우수하다는 것을 말하므로 생략한다. 

 

마지막으로 Inference latency 측면에서 PPTOD 모델(plug-and-play)이 기존의 cascaded 방식의 모델들보다 비약적으로 빨라졌다. 

 

200ms도 느리진 않지만 서비스 측면에서는 14배 빠른 end-to-end (plug-and-play) 모델이 경쟁력을 보일 수 있을 것이다. 

 

끝으로 이 논문의 핵심 컨트리뷰션은 기존의 챗봇이 NLU, DST, DP, NLG와 같이 모듈화되어 있었다면 이 모듈화된 것을 end-to-end 방식으로 바꿨다는 데 있다. 또 이를 통합함으로써 자연스럽게 모델 추론 속도가 향상되었다는 점이다. end-to-end 모델의 우수성은 이 논문의 PPTOD 모델로 증명되었다. 다만 TOD 챗봇을 위해 다양한 도메인의 데이터셋이 만들어진다면 앞으로 많은 end-to-end 연구가 이루어질 수 있을 것이라 생각한다.

multi-domain dialogue system의 기본적인 내용을 살펴보다 보니 2011년 논문까지 거슬러 올랐다. 이 논문은 multi-domain 챗봇 시스템에서 도메인을 어떻게 결정할 것이냐?는 문제에 대한 답을 한다. 이런 multi-domain 챗봇 시스템은 기존 single-domain 챗봇 시스템에서 새로운 도메인이 추가될 때 확장성이 요구되지만 이를 어떻게 충족시킬 수 있을까하는 물음으로부터 시작된 연구다. multi-domain 챗봇 시스템이 사용했던 기존의 방식은 사용자 발화에 대한 도메인을 결정하고 그 도메인에 해당하는 하위도메인 시스템이 결정되는 구조였다. 하지만 이는 한 가지 문제점이 있다. 만약 multi-turn일 경우 다음 이야기 하는 발화의 도메인이 이전에 했던 발화의 도메인과 의도치 않게 바뀔 수 있다는 것이다. 따라서 보통 이전 발화 정보에 가중치를 줘서 도메인 결정에 함께 사용했다고 한다. 하지만 이 방법 또한 한계점이 있다고 하는데 여기에 대해 언급된 바는 없었다. (유추해 보자면 가중치와는 무관하게 도메인이 추가될 때마다 새로 학습해야하는 것이 아닐까?)

 

다만 이 도메인 결정 문제를 해결하기 위해 3가지 요소를 사용했다. 단어, 화행, 이전 발화 도메인 정보다. 단어는 화자의 발화에 포함된 단어이고 화행은 용어에서 직관적인 이해가 되지 않았지만 예를 들어 만약 restaurant 도메인이면 ask_food, ask_location 등이 화행이다. 이전 발화 도메인 정보는 말 그대로 어떤 도메인인지를 말한다. 가령 이 논문에서는 아래와 같이 Weather, Resfinder, Songfinder, TVGuide라는 4가지 도메인을 사용했다. 

딥러닝 연구가 막 약동하기 시작할 때 쯤이여서 사용한 모델은 통계기반의 최대 엔트로피 모델을 사용했다. 결과적으로 이전 발화도메인 정보와 화행을 추가했을 때 아래와 같이 성능이 제일 높게 나왔다고 한다.

데이터셋이 적어서 일반화된 성능이라 보기는 어려울 것으로 보인다. 다만 이 논문을 통해 얻게 된 인사이트는 챗봇 시스템 구현에 있어서 이전 발화 도메인과 화행 정보를 고려해서 무분별한 도메인 전환이 이뤄지지 않도록 해야한다는 것이다. 아래의 대화 예제처럼 말이다.

 

또 도메인 결정 문제도 하나의 연구 주제가 될 수 있음을 알게 되었다. 최근 연구들에서는 어떻게 도메인 결정 문제를 해결하고 있을지 궁금해지는 대목이다.

 

발생 에러 메시지

return torch.stack(batch, 0, out=out)
RuntimeError: stack expects each tensor to be equal size, but got [3, 64] at entry 0 and [4, 64] at entry 1

 

데이터로더로부터 데이터를 가져와 학습시키기 전 발생한 오류다. 텐서 사이즈가 동일하지 않다고 한다. 원인은 배치 사이즈 별로 데이터가 묶이지 않아 발생한다고 한다. 직접적으로 텐서 사이즈를 조작 해주어도 잘 해결되지 않았다. 해결 방법은 DataLoader에 collate_fn 파라미터를 사용해줌으로써 학습을 시작할 수 있었다. collate_fn 파라미터는 텐서 간의 사이즈를 조절하는 역할을 한다. 아래와 같이 함수 하나를 만들어 주고 DataLoader(dataset, ..., collate_fn = collate_fn)과 같은 형식으로 입력해주면 문제 해결 가능하다.

 

def collate_fn(batch):
    return tuple(zip(*batch))

 

RuntimeError: CUDA error: device-side assert triggered

Traceback (most recent call last):
  File "main.py", line 70, in <module>
    trainer.train()
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\transformers\trainer.py", line 463, in train
    tr_loss += self._training_step(model, inputs, optimizer)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\transformers\trainer.py", line 580, in _training_step
    outputs = model(**inputs)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\nn\modules\module.py", line 1051, in _call_impl
    return forward_call(*input, **kwargs)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\transformers\modeling_bert.py", line 1284, in forward
    loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\nn\modules\module.py", line 1051, in _call_impl
    return forward_call(*input, **kwargs)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\nn\modules\loss.py", line 1121, in forward
    ignore_index=self.ignore_index, reduction=self.reduction)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\nn\functional.py", line 2824, in cross_entropy
    return torch._C._nn.cross_entropy_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
RuntimeError: CUDA error: device-side assert triggered

 

에러를 해결하는 과정에서 pytorch 버전 변경, cuda 버전 변경, 환경변수 설정 등을 잘해주라는 답변으로 인해 시간을 다소 낭비했다. 나의 경우는 pre-trained 모델 로딩할 때 매개변수로 num_labels를 입력해주어야 한다는 것을 알지 못해 발생한 문제였다. 아래와 같이 데이터셋의 라벨 개수와 맞도록 입력해주면 된다.

 

model = BertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=4).to(device)

(만약 위와 같이 num_labels를 설정해주어도 에러가 발생한다면 라벨의 인덱스를 확인해 0부터 시작하는지 확인해야 한다. 1부터 시작하도록 설정했더니 위와 같은 에러가 또 발생했었음)

 

다음은 위 에러를 조우하기 전 발생했던 에러다. 

RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`

 

Traceback (most recent call last):
  File "main.py", line 65, in <module>
    trainer.train()
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\transformers\trainer.py", line 463, in train
    tr_loss += self._training_step(model, inputs, optimizer)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\transformers\trainer.py", line 592, in _training_step
    loss.backward()
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\_tensor.py", line 255, in backward
    torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
  File "C:\Users\roytravel\anaconda3\envs\kochat\lib\site-packages\torch\autograd\__init__.py", line 149, in backward
    allow_unreachable=True, accumulate_grad=True)  # allow_unreachable flag
RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`

 

이는 아래 환경변수 설정으로 해결하였다.

import os
os.environ['CUDA_LAUNCH_BLOCKING'] = "1"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

 

1. 분산 학습 개요

최근 몇 년간 Large Language Model을 만드는 추세가 계속해서 이어지고 있다. 이런 거대 모델의 경우 파라미터를 역전파로 업데이트하기 위해 많은 양의 메모리와 컴퓨팅 파워가 필요하다. 따라서 여러 프로세서에 분산시켜 모델을 학습하는 분산 학습이 필요하다. 분산 학습을 통해 CPU 또는 GPU 상의 학습 속도 향상을 이룰 수 있다. 많은 사람들이 사용하는 딥러닝 라이브러리인 파이토치에서 이런 분산 학습을 돕는 아래 API들이 있다.

 

1. torch.multiprocessing

  • 여러 파이썬 프로세스를 생성하는 역할. 일반적으로 CPU나 GPU 코어 수 만큼 프로세스 생성 가능

2. torch.distributed

  • 분산 학습을 진행할 수 있도록 각 프로세스 간의 통신을 가능하게 하는 일종의 IPC 역할

3. torch.utils.data.distributed.DistributedSampler

  • 학습 데이터셋을 프로세스 수 만큼 분할해 분산 학습 세션의 모든 프로세스가 동일한 양의 데이터로 학습하도록 만드는 역할.
  • 프로세스 수만큼 나누기 위해 world_size라는 인자를 사용. 

4. torch.nn.parallel.DistributedDataParallel

해당 API는 내부적으로 5가지 동작이 이뤄짐

  • 분산 환경에서 각 프로세스마다 고유한 모델 사본이 생성됨.
  • 고유 모델 사본 별 자체 옵티마이저를 갖고, 전역 이터레이션과 동기화됨
  • 각 분산 학습 이터레이션에서 개별 loss를 통해 기울기가 계산되고, 각 프로세스의 기울기 평균을 구함
  • 평균 기울기는 매개변수를 조정하는 각 모델 복사본에 전역으로 역전파됨.
  • 전역 역전파 때문에 모든 모델의 매개변수는 이터레이션마다 동일하도록 자동으로 동기화됨. 

 

2. 분산 학습 루틴 정의

그렇다면 실제로 위 API를 이용해 어떻게 분산 학습 루틴을 정의할 수 있을까? 아래 예시 코드를 통해 알아보자.

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

import torch.multiprocessing as mp
import torch.distributed as dist

import os
import time
import argparse

class ConvNet(nn.Module):
	pass
    
def train(gpu_num, args):
    rank = args.machine_id * args.num_gpu_processes + gpu_num                        
    dist.init_process_group(backend='nccl', init_method='env://', world_size=args.world_size, rank=rank) 
    torch.manual_seed(0)
    model = ConvNet()
    torch.cuda.set_device(gpu_num)
    model.cuda(gpu_num)
    criterion = nn.NLLLoss().cuda(gpu_num) # nll is the negative likelihood loss
    
    train_dataset = datasets.MNIST('../data', train=True, download=True,
                                   transform=transforms.Compose([
                                       transforms.ToTensor(),
                                       transforms.Normalize((0.1302,), (0.3069,))]))  
                                       
    train_sampler = torch.utils.data.distributed.DistributedSampler(
        train_dataset,
        num_replicas=args.world_size,
        rank=rank
    )
    train_dataloader = torch.utils.data.DataLoader(
       dataset=train_dataset,
       batch_size=args.batch_size,
       shuffle=False,            
       num_workers=0,
       pin_memory=True,
       sampler=train_sampler)
       
    optimizer = optim.Adadelta(model.parameters(), lr=0.5)
    model = nn.parallel.DistributedDataParallel(model, device_ids=[gpu_num])
    model.train()
    for epoch in range(args.epochs):
        for b_i, (X, y) in enumerate(train_dataloader):
            X, y = X.cuda(non_blocking=True), y.cuda(non_blocking=True)
            ... 
            if b_i % 10 == 0 and gpu_num==0:
                print (...)
            
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--num-machines', default=1, type=int,)
    parser.add_argument('--num-gpu-processes', default=1, type=int)
    parser.add_argument('--machine-id', default=0, type=int)
    parser.add_argument('--epochs', default=1, type=int)
    parser.add_argument('--batch-size', default=128, type=int)
    args = parser.parse_args()
    
    args.world_size = args.num_gpu_processes * args.num_machines                
    os.environ['MASTER_ADDR'] = '127.0.0.1'              
    os.environ['MASTER_PORT'] = '8892'      
    start = time.time()
    mp.spawn(train, nprocs=args.num_gpu_processes, args=(args,))
    print(f"Finished training in {time.time()-start} secs")
    
if __name__ == '__main__':
    main()

 

ConvNet 클래스는 코드가 길어지지 않도록 한 것으로 모종의 컨볼루션 레이어가 들어있다고 가정한다. 분산 학습의 루틴이 되는 로직의 핵심은 train 함수에 들어있다. 살펴보면 가장 먼저 첫번째로 rank가 할당된다. rank는 전체 분산시스템에서 프로세스 순서를 지칭하는 것이다. 예를 들어 4-CPU를 가진 시스템 2개가 있다면 8개의 프로세스를 생성할 수 있고 각 프로세스는 0~7번까지 고유 번호를 가질 것이다. rank를 구하기 위해 사용하는 식은 $rank = n*4 + k$이다. 여기서 n은 시스템 번호(0, 1)이 되고, k는 프로세스 번호(0, 1, 2, 3)이다. 참고로 rank가 0인 프로세스만 학습에 대한 로깅을 출력한다. 그 이유는 rank가 0인 프로세스가 다른 프로세스와의 통신의 주축이 되기 때문이다. 만약 그렇지 않다면 프로세스 개수만큼 로그가 출력될 것이다.

 

두 번째로 dist.init_process_group 메서드가 보인다. 이는 분산 학습을 진행하는 각 프로세스 간의 통신을 위해 사용한다.  정확히는 매개변수로 들어가는 backend 인자가 그 역할을 한다. pytorch에서 지원하는 backend 인자에는 Gloo, NCCL, MPI가 있다. 간단히 언급하면 주로 Gloo는 CPU 분산 학습에 NCCL은 GPU 분산 학습에, MPI는 고성능 분산 학습에 사용된다. init method는 각 프로세스가 서로 탐색하는 방법으로 URL이 입력되며 기본값으로 env://이 설정된다. world_size는 분산 학습에 사용할 전체 프로세스 수다. world_size 수 만큼 전체 학습 데이터셋 수가 분할된다.

 

세 번째로 torch.utils.data.distributed.DistributedSampler다. world_size 수 만큼 데이터셋을 분할하고 모든 프로세스가 동일한 양의 데이터셋을 갖도록 한다. 이후에 DataLoader가 나오는데 shuffle=False로 설정한 것은 프로세스 간 처리할 데이터셋의 중복을 피하기 위함이다. 

 

네 번째로 torch.nn.parallel.DistributedDataParallel다. 분산 환경에서 사용할 각각의 모델 복사본을 생성한다. 생성된 각 모델 복사본은 각자 옵티마이저를 갖고, loss function으로부터 기울기를 계산하고 rank 0을 가지는 프로세스와 통신하여 기울기의 평균을 구하고 rank 0을 갖는 프로세스로부터 평균 기울기를 받아 역전파를 수행한다. 이 DistributedDataparallel을 사용하면 각각 독립된 프로세스를 생성하므로 파이썬 속도 한계의 원인인 GIL 제약이 사라져 모델 학습 속도를 늘릴 수 있다는 장점이 있다. 

 

다섯 번째로 MASTER_ADDR은 rank가 0인 프로세스를 실행하는 시스템의 IP 주소를 의미하고 MASTER_PORT는 그 장치에서 사용 가능한 PORT를 의미한다. 이는 앞서 언급했듯 rank 0인 시스템이 모든 백엔드 통신을 설정하기 때문에 다른 프로세스들이 호스팅 시스템을 찾기 위해 사용한다. IP에 따라 local이나 remote를 설정해 사용할 수 있다. 

 

마지막으로 데이터로더에 pin_memory=True는 데이터셋이 로딩 된 장치(ex: CPU)에서 다양한 장치(GPU)로 데이터를 빠르게 전송할 수 있도록 한다. 예를 들어 데이터셋이 CPU가 사용하는 고정된 page-lock 메모리 영역에 할당되어 있다면 GPU는 이 CPU의 page-lock 메모리 영역을 참조하여 학습도중 필요한 데이터를 복사해 사용한다. pin_memory=True는 참고로 학습 루틴 상에서 non_blocking=True라는 매개변수와 함께 동작한다. 결과적으로 GPU 학습 속도가 향상되는 효과를 가져온다.

훈련이 완료된 ML/DL 모델은 실제 서비스에 적용하기 위해 프로덕션 레벨로 배포하고 서빙해야한다. 이를 위한 모델 서빙 종류는 로컬에 플라스크로 모델 서버 구축, 도커로 모델 서빙 환경 구축, 토치서브로 모델 서빙하는 방법, 클라우드로 모델 서빙하는 방법이 있다. 모델 서빙은 모델 훈련이 완료되고 저장되어 있어 재사용가능 할때 이루어지므로 간단히 모델 저장 방법에 대해 언급하고 넘어가자.

 

파이토치 모델 저장 및 로딩 방법

파이토처리 모델을 저장하고 로딩하는 방법은 2가지가 있다. 첫 번째 방법은 가장 간단한 방식으로 전체 모델 객체를 저장하고 로딩하는 방식이다.

torch.save(model, PATH)
model = torch.load(PATH)

모델 전체를 저장하므로 간편하다. 하지만 이 방식은 경우에 따라 문제가 발생할 수 있다. 그 이유는 모델 객체 전체를 저장하기 때문에 모델 객체 내부에 매개변수, 모델 클래스, 디렉터리 구조까지 함께 저장한다. 따라서 만약 추후 디렉터리 구조라도 변경된다면 모델 로딩에 실패하게 되고 문제 해결이 어려워질 수 있다. 따라서 모델 매개변수만 저장하는 아래 두 번째 방법을 사용하는 것이 좋다.

torch.save(model.state_dict(), PATH)
model = ConvNet()
model.load_state_dict(torch.load(PATH))

모델 매개변수만 저장하기 위해 state_dict() 메서드를 사용하고, 추후 빈 모델 객체를 인스턴스화해 빈 모델 객체에 매개 변수를 로딩해 사용하는 방법이다. 간단히 파이토치 모델 저장하는 방법을 알아보았으니 다음으로 이 모델을 배포하여 서빙하는 방법에 대해 알아보자. 

 

1. 플라스크로 로컬에 모델 서버 구축하기

가장 간단하게 설치해 사용할 수 있는 플라스크를 서버로 사용한다. 플라스크 서버에 입력(추론 요청)이 들어오면 추론한 결과를 출력값으로 하여 되돌려 보내준다. 중요한 것은 플라스크 서버 내부에서 추론해 결과를 돌려주는 일이므로 추론 함수 작성이 필요하다. 예시를 위해 MNIST 숫자를 예측하는 모델이라 가정한다. 그렇다면 다음과 같은 추론 함수를 작성할 수 있다. 

def run_model(input_tensor):
    model_input = input_tensor.unsqueeze(0)
    with torch.no_grad():
        model_output = model(model_input)[0]
    model_prediction = model_output.detach().numpy().argmax()
    return model_prediction

 

이 추론 함수를 플라스크 서버 내부에서 동작시키려면 플라스크 서버의 코드는 다음과 같은 형태로 작성할 수 있다. 

 

import os
import json
import numpy as np
from flask import Flask, request

import torch
import torch.nn as nn
import torch.nn.functional as F

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.cn1 = nn.Conv2d(1, 16, 3, 1)
        self.cn2 = nn.Conv2d(16, 32, 3, 1)
        self.dp1 = nn.Dropout2d(0.10)
        self.dp2 = nn.Dropout2d(0.25)
        self.fc1 = nn.Linear(4608, 64) # 4608 is basically 12 X 12 X 32
        self.fc2 = nn.Linear(64, 10)
 
    def forward(self, x):
        x = self.cn1(x)
        x = F.relu(x)
        x = self.cn2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dp1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dp2(x)
        x = self.fc2(x)
        op = F.log_softmax(x, dim=1)
        return op
    
model = ConvNet()
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()

def run_model(input_tensor):
    model_input = input_tensor.unsqueeze(0)
    with torch.no_grad():
        model_output = model(model_input)[0]
    model_prediction = model_output.detach().numpy().argmax()
    return model_prediction

def post_process(output):
    return str(output)

app = Flask(__name__)

@app.route("/test", methods=["POST"])
def test():
    data = request.files['data'].read()
    md = json.load(request.files['metadata'])
    input_array = np.frombuffer(data, dtype=np.float32)
    input_image_tensor = torch.from_numpy(input_array).view(md["dims"])
    output = run_model(input_image_tensor)
    final_output = post_process(output)
    return final_output

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8890)

 

위와 같이 작성된 플라스크 서버를 구동시키면 입력을 기다릴 것이고 입력 request를 날려주는 함수를 하나 작성하면 다음과 같이 작성할 수 있다.

 

import io
import json
import requests
from PIL import Image
from torchvision import transforms

def image_to_tensor(image):
    gray_image = transforms.functional.to_grayscale(image)
    resized_image = transforms.functional.resize(gray_image, (28, 28))
    input_image_tensor = transforms.functional.to_tensor(resized_image)
    input_image_tensor_norm = transforms.functional.normalize(input_image_tensor, (0.1302,), (0.3069,))
    return input_image_tensor_norm

image = Image.open("./digit_image.jpg")
image_tensor = image_to_tensor(image)
dimensions = io.StringIO(json.dumps({'dims': list(image_tensor.shape)}))
data = io.BytesIO(bytearray(image_tensor.numpy()))

res = requests.post('http://localhost:8890/test', files={'metadata': dimensions, 'data' : data})
response = json.loads(res.content)

print("Predicted digit :", response)

 

위와 같이 작성한 추론 요청 함수를 실행시키면 플라스크 모델 서버로부터 추론한 결과를 받을 수 있게 된다. 하지만 이와 같이 구축된 모델 추론 파이프라인을 다른 환경에서 동일하게 구축하려면 수동으로 동일한 라이브러리 설치부터 폴더구조를 맞춰주고 파일을 복사하는 등의 작업이 필요하다. 때문에 이 환경을 그대로 복제하여 다른 환경에서도 사용할 수 있도록 하는 확장성이 필요하다. 이 때 사용할 수 있는 것이 도커다. 도커를 통해 손쉽게 위 환경을 복제하고 실행하는 방법에 대해 알아보자. 

 

 

2. 도커로 모델 서빙 환경 구축하기

도커를 사용하면 서버 구축에 사용되었던 소프트웨어 환경을 손쉽게 만들 수 있다.

 

2.1 Dockerfile 만들기

도커를 사용하기 위해서는 가장 먼저 Dockerfile을 만들어야 한다. Dockerfile을 실행하면 결과적으로 위 플라스크로 구축했던 모델 서빙환경이 그대로 재현되어야 한다. Dockerfile 실행하면 도커 이미지가 생성되고 이 과정을 이미지 빌드라고 한다. 이를 위해 Dockerfile에 다음과 같은 스크립트를 작성할 수 있다.

FROM python:3.8-slim 

RUN apt-get -q update && apt-get -q install -y wget

COPY ./server.py ./
COPY ./requirements.txt ./

RUN wget -q https://raw.githubusercontent.com/wikibook/mpytc/main/Chapter10/convnet.pth
RUN wget -q https://github.com/wikibook/mpytc/raw/main/Chapter10/digit_image.jpg

RUN pip install --no-cache-dir -r requirements.txt

USER root
ENTRYPOINT ["python", "server.py"]

 

간단히 각 명령에 대해 설명하자면 FROM을 통해 도커에 python3.8이 포함된 표준 리눅스 OS를 가져오도록 지시하고, RUN을 통해 업데이트 하고 wget을 다운로드 한다. 이후 COPY를 통해 로컬에 만들어두었던 서버 파일과 환경구축에 필요한 requirements.txt 파일을 복사해준다. 이후 추론에 필요로한 모델과 예제 이미지를 다운로드 받고, pip install을 통해 파이썬 라이브러리를 모두 설치해준다. 참고로 requirements.txt에는 다음과 같은 라이브러리가 들어있다. (만약 동작하지 않는다면 Flask를 업데이트 할 것) 

torch==1.5.0
torchvision==0.5.0
Pillow==6.2.2
Flask==1.1.1

USER를 통해 루트 권한을 부여하고 ENTRYPOINT를 통해 python server.py 명령이 실행되면 도커상에서 플라스크 모델 서버가 실행된다. 

 

2.2 도커 이미지 빌드

도커 파일이 작성되었으면 이를 빌드해주어 도커 이미지로 만들어주어야 한다. 이를 위해 다음과 같은 명령어를 사용한다.

docker build -t <tag name> .

여기서 <tag name>은 임의 설정이다. 

 

2.3 도커 이미지 배포 (실행)

도커 이미지 빌드를 통해 만들어진 도커 이미지를 다음과 같은 명령 실행을 통해 배포가 가능하다.

docker run -p 8890:8890 <tag name>

 

위 명령이 실행되면 다음과 같이 도커 이미지가 실행되어 모델 서빙이 가능하도록 플라스크 서버가 구동되는 것을 확인할 수 있다.

 

구동된 플라스크 서버에 다시 추론을 요청하는 request를 날리면 이에 대한 응답이 결과로 반환되는 것을 확인할 수 있다.

 

도커 실행을 중지하려면 현재 실행 중인 도커 컨테이너를 확인해야 하며 이는 docker ps -a 명령으로 확인할 수 있다. 그러면 CONTAINER ID를 확인할 수 있고 이를 복사하여 docker stop <CONTAINER ID> -t 0을 실행시켜주면 도커 컨테이너가 중지된다. 만약 도커 컨테이너를 삭제하고 싶다면 docke rm <CONTAINER ID> 명령을 실행하면 된다. 빌드했던 도커 이미지까지 삭제하려면 docker images 명령을 통해 <tag name>을 확인하고 docker rmi <tag name>을 통해 삭제할 수 있다.

 

3. 토치서브로 모델 서빙하기

torchserve는 파이토치 모델 서버 라이브러리다. 메타와 AWS에서 만들었고, 파이토치 모델 배포를 도와주는 역할을 한다. torchserve를 사용하기 위해서 Java 11 SDK 설치와, pip install torchserve torch-model-archiver 명령 실행을 통해 라이브러리 설치가 우선적으로 필요하다. 여기서 torch-model-archiver는 압축 라이브러리다. 입력값을 3개 받아 .mar 파일로 만들어주는 역할을 한다. 입력값 3개란 1. 모델 클래스 파일 2. 훈련된 모델 파일 3. 핸들러(전처리, 후처리) 파일이다.

 

1. 클래스 파일은 모델 레이어가 구성된 파일로, 앞서 플라스크 서버 구축때 사용할 때 정의했던 ConvNet 클래스를 별도 파일로 만들어준 것이다. 

2. 훈련된 모델 파일은 convnet.pth와 같이 모델 학습이 완료된 파일이며

3. 핸들러는 torchvision의 transform 클래스와 같이 전처리를 할 수 있는 로직이나 별도의 후처리 로직이 담긴 파일이다.

 

torch-model-archiver를 사용해 아래 명령을 실행시켜주면 목표로하는 convnet.mar 파일이 생성된다. 

torch-model-archiver --model-name convnet --version 1.0 \
--model-file ./convnet.py --serialized-file ./convnet.pth --handler ./convnet_handler.py

이후 아래 명령을 통해 새 디렉터리를 생성하고 생성한 convnet.mar 파일을 옮겨준다.

mkdir model_store
mv convnet.mar model_store

이후 토치서브를 이용해 모델 서버를 런칭하는 명령 실행을 통해 서버를 구동해준다.

torchserve --start --ncs --model-store model_store --models convnet.mar

이후 curl을 활용해 토치서브 모델 서버에 추론 요청을 수행할 수 있다. 핸들러에 의해 어떤 입력이라도 전처리되어 텐서로 바뀌기 대문에 별도의 코드를 작성하지 않아도 된다.

curl http://127.0.0.1:8080/predictions/convnet -T ./digit_image.jpg

결과적으로 추론한 결과 숫자가 터미널에 출력될 것이다. 만약 토치서브로 구동한 서버를 종료하고 싶다면 torchserve --stop 명령을 통해 중지할 수 있다. 덧붙여 만약 서버의 모델 서빙 여부를 확인하기 위한 핑을 보내는 명령은 다음과 같이 사용할 수 있다. 참고로 포트 정보는 torchserve 실행에 의해 서버가 실행될 때 로그를 통해 확인 가능하다.

curl http://localhost:8081/models

 

 

4. 토치스크립트로 범용 파이토치 모델 만들기

지금까지는 로컬 플라스크 서버, 도커 환경, 토치서브 환경을 통해 모델 서버를 구현했다. 하지만 이는 파이썬 스크립트 환경에서 이루어진 것이다. 훈련한 모델이 반드시 훈련했던 환경에서 모델 서빙이 이뤄진다 할 수 없다. 파이썬이 실행되지 않는 외부환경에서도 실행될 수 있다. 따라서 파이토치 모델을 C++과 같은 타 언어에서도 실행될 수 있도록 중간 표현으로 만들 필요성이 있다. 이 때 토치스크립트를 사용할 수 있다. 토치스크립트는 별도의 라이브러리는 아니고 파이토치 내부에서 모델 연산 최적화를 위해 사용하는 JIT compiler에서 사용된다. 토치스크립트는 간단히 torch.jit.script(model) 또는 torch.jit.trace(model, input)를 실행시켜 만들 수 있다. 

 

파이토치 모델을 토치스크립트로 컴파일하기 위해선, 언급했듯 두 가지 방식이 존재한다. 첫 번째는 trace 방식이고 두 번째는 script 방식이다. trace 방식을 위해선 모델과 더미(임의) 입력값이 필요하다. 더미 입력값을 모델에 넣어서 입력값이 어떻게 흐르는지 추적해 기록한다. trace 방식을 사용해 모델을 만든다면 다음과 같은 형태로 만들 수 있다.

model = ConvNet()
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()
for p in model.parameters():
    p.requires_grad_(False)
demo_input = torch.ones(1, 1, 28, 28)
traced_model = torch.jit.trace(model, demo_input)
# print (traced_model.graph)
# print(traced_model.code)
torch.jit.save(traced_model, 'traced_convnet.pt')
loaded_traced_model = torch.jit.load('traced_convnet.pt')

중간에 있는 torch.jit.trace() 메서드를 통해 토치스크립트 형식의 객체를 만들어주는 것이 핵심이다. 이후 모델을 저장하게 되면 C++과 같은 다른 언어에서도 파이토치 모델을 로딩해 추론할 수 있다. 위를 살펴보면 model과 loaded_trace_model 두 개가 있는데 향후 모델에 입력값을 넣고 추론해보면 추론 결과는 완벽히 동일하게 출력된다.

 

하지만 이러한 trace 방식을 이용하면 한 가지 큰 문제가 발생할 수 있다. 예를 들어 모델 순전파가 if나 for문과 같은 제어 흐름으로 구성된다면 trace는 여러 가능한 경로 중 하나만 토치 스크립트로 렌더링할 것이다. 따라서 기존 모델과 동일성을 보장할 수 없다. 이를 해결하기 위해서는 script 방식을 사용해 토치 스크립트로 컴파일 해야 한다. script 방식도 위 trace 방식과 동일하다. 다만 차이점이 있다면 더미 입력이 사용되지 않는 것이 특징이다. 

model = ConvNet()
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()
for p in model.parameters():
    p.requires_grad_(False)
scripted_model = torch.jit.script(model)
# print(scripted_model.graph)
# print(scripted_model.code)
torch.jit.save(scripted_model, 'scripted_convnet.pt')
loaded_scripted_model = torch.jit.load('scripted_convnet.pt')

마찬가지로 중간에 있는 torch.jit.script() 메서드를 통해 토치스크립트 형식의 객체를 만들어주는 것이 핵심이다. 또 model과 loaded_scripted_model에 모두 입력값을 넣고 추론해보면 추론 결과가 동일하게 출력된다. 이렇게 script 방식을 사용해 토치스크립트 코드로 만들면 trace 방식에 비해 정확성을 더 얻을 수 있다. 다만 script 방식이 가지는 단점이 있다면 파이토치 모델이 토치스크립트에서 지원하지 않는 기능을 포함하면 동작할 수 없다. 이 때는 모델 순전파에 if for문 로직을 제거한 뒤 trace 방식을 사용해야 한다.

 

결론을 이야기하자면 사실상 토치스크립트는 모델 서빙을 위해 필수적으로 사용해야 한다. 그 이유는 파이썬 내부에서는 전역 인터프리터 잠금(GIL)이 설정되어 있어 한 번에 한 쓰레드만 실행될 수 있어 연산 병렬화가 불가능하기 때문이다. 하지만 토치스크립트를 통해 중간 표현으로 바꾸어 범용 형식으로 만들어두면 연산 병렬화가 가능해져 모델 서빙 속도 향상이 가능해진다. 

 

5. ONNX로 범용 파이토치 모델 만들기

토치스크립트와 마찬가지로 ONNX 프레임워크를 사용해도 파이토치 모델을 범용화할 수 있다. 그렇다면 토치스크립트와의 차이점은 무엇일까? 토치스크립트는 파이토치에서만 사용할 수 있다면 ONNX는 텐서플로우나 이외의 딥러닝 라이브러리와 같이 더 넓은 범위에서 표준화 시킬 수 있다. 예를 들어 파이토치로 만든 모델을 텐서플로우에서 로드하여 사용할 수 있다. 이를 위해 ONNX 라이브러리 설치가 필요하다. pip install onnx onnx-tf 명령을 통해 필요한 라이브러리를 설치할 수 있다. 

 

학습한 파이토치 모델을 ONNX 포맷으로 바꾸어 저장하기 위해서는 다음과 같은 예시 코드를 사용할 수 있다.

model = ConvNet()
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()
for p in model.parameters():
    p.requires_grad_(False)
demo_input = torch.ones(1, 1, 28, 28)
torch.onnx.export(model, demo_input, "convnet.onnx")

토치스크립트에서 trace 방식과 마찬가지로 더미 입력값이 필요하다. 핵심은 torch.onnx.export() 메서드를 사용해 onnx 포맷 형식으로 파일을 생성하는 것이다. 이후 텐서플로우 모델로 변환하기 위해서는 다음과 같은 예시 코드를 사용할 수 있다.

 

import onnx
from onnx_tf.backend import prepare

model_onnx = onnx.load("./convnet.onnx")
tf_rep = prepare(model_onnx)
tf_rep.export_graph("./convnet.pb")

 

tf.rep.export_graph() 메서드가 실행되면 텐서플로우에서 사용 가능한 모델 파일인 convnet.pb가 생성된다.

 

이후 텐서플로우에서 사용하기 위해 모델 그래프를 파싱하는 과정이 필요하다. (참고로 TF 1.5 버전) 

with tf.gfile.GFile("./convnet.pb", "rb") as f:
    graph_definition = tf.GraphDef()
    graph_definition.ParseFromString(f.read())
    
with tf.Graph().as_default() as model_graph:
    tf.import_graph_def(graph_definition, name="")
    
for op in model_graph.get_operations():
    print(op.values())

 

그래프를 파싱하고 그 결과를 출력하면 아래와 같이(간략화된) 그래프의 입출력 노드 정보를 확인할 수 있다. 

(<tf.Tensor 'Const:0' shape=(16,) dtype=float32>,)
...
(<tf.Tensor 'input.1:0' shape=(1, 1, 28, 28) dtype=float32>,)
...
(<tf.Tensor '18:0' shape=(1, 10) dtype=float32>,)

 

입출력 노드 정보를 기반으로 모델의 입출력을 지정해줄 수 있고 텐서플로우를 통해 추론하면 아래와 같이 예측 확률분포가 출력되는 것을 확인할 수 있다. 

model_output = model_graph.get_tensor_by_name('18:0')
model_input = model_graph.get_tensor_by_name('input.1:0')

sess = tf.Session(graph=model_graph)
output = sess.run(model_output, feed_dict={model_input: input_tensor.unsqueeze(0)})
print(output)

> [[-9.35050774e+00 -1.20893326e+01 -2.23922171e-03 -8.92477798e+00
  -9.81972313e+00 -1.33498535e+01 -9.04598618e+00 -1.44924192e+01
  -6.30233145e+00 -1.22827682e+01]]

 

지금까지의 전체 과정을 요약하자면 파이토치 모델을 서빙하는 방법과 모델을 범용화 시키는 방법에 대해 알아봤다. 모델 서빙을 위해서는 크게 3가지로 로컬에서 플라스크 서버를 실행시켜 서빙하는 방법, 플라스크 서버를 도커 이미지화 하는 방법, 토치서브를 통해 서빙하는 방법이 있었다. 도커 이미지화를 위해선 Dockerfile을 생성해주고 몇몇 명령어를 실행시켜주었고, 토치서브를 실행시키기 위해 torch-model-archiver를 통해 .mar 파일로 만들어준 뒤, 서버를 실행시켜 추론 결과를 응답해주었다. 다음으로 파이토치 모델의 범용화를 위해 토치스크립트를 사용해 모델 범용화하는 방법과 ONNX를 통해 모델 범용화하는 방법이다.

 

잘 이뤄지는 모델 학습이  중요한 만큼 서비스와 제품화를 위해 모델을 배포하고 서빙하는 과정도 중요하다. 특히 실제 사용되는 서비스에서는 속도가 중요하므로 토치스크립트나 ONNX를 통해 모델을 범용 포맷으로 만듦으로써 파이썬 속도 한계의 원인인 전역 인터프리터 잠금(GIL)으로부터 벗어날 수 있다. 이후 플라스크 서버나 토치서브 환경을 도커 컨테이너로 만들어 둔다면 개발 속도 측면이나 효율성 등의 측면에서 많은 이점을 얻을 수 있을 것이다. 글을 작성하며 사용한 예시 코드의 전체 코드는 아래 링크를 참조하였다.

 

코드: https://github.com/wikibook/mpytc/tree/main/Chapter10

 

Reference

[1] 실전! 파이토치 딥러닝 프로젝트

 

책 읽을 때 좋은 습관에 대해 저자들의 다양한 의견을 종합해 15가지로 추린 내용들.

 

* 경제 경영서 읽는 습관 15가지

 

1. 목차, 서문, 후기를 우선 읽고 첫 번째 챕터나 특히 관심가는 챕터 읽어라.

> 책 읽을지 말지 판단 가능하다. 또 제한된 시간 속에서 효율성 확보 할 수 있다.

 

2. 책과 관련 있는 영화 찾아봐라.

> 비즈니스 현장을 간접체험 가능한 영화 많다. 책 & 영화는 생각 이상으로 멋진 콤비다.

 

3. 나름대로 서평 작성하거나 주위 사람에게 책 내용을 말해줘라.

> 가르칠 때 가장 잘배운다. 주위 사람에게 책 내용을 부담없이 말해줄 수 있으면 좋다.

 

4. 가방에 가볍게 읽을 수 있는 책과 숙고 가능한 책 두 권 들고 다니면 좋다.

> 상황에 따라 마음가짐이 달라질 수 있기 때문이다.

 

5. 아웃풋을 전제로 읽어라

> 아웃풋이란 '확실한' 목표다. 가령 목표에는 글쓰기, 자랑, 가르침, 문제해결 등이 있을 수 있다.

 

6. 다 읽어야 한단 마음을 버려라.

> 죄책감 갖지 마라. 저자의 잘못이라 생각하면 편하다.

 

7. 비판적으로 읽어라.

> 저자가 다 옳지 않다. 잡혀먹지 마라 책에. 잡아 먹어라.

 

8. 읽는 것 보다 기억이 중요하다.

> 필사만큼 중요한 게 없다. 시간은 들어도 책 내용을 더 잘 기억한다.

 

9. 사색의 시간이 필요하다.

> 음식처럼 소화와 흡수가 필요하므로 사색을 통해 체화해야 한다.

 

10. 책 핵심을 요약해라.

> 중요 내용 지나치지 않고 재점검 할 수 있어 좋다. 또 요약 과정이 지식 체화과정이다.

 

11. 관심 주제/분야 쫓아서 읽어라.

> 경제, 경영, 금융에 관심 있다면 《파이낸셜타임스》에서 매년 발표하는 BEST 15를 읽으면 대체로 만족스러울 것이다.

 

12. 목차를 상기하며 읽어라.

> 저자의 논리 전개과정이 모두 요약되어 있다.

 

13. 미디어 서평을 읽어라.

> 거의 모든 신문에는 서평 코너가 있다. 아무 책이나 올리지 않고 의미있거나 사회적으로 관심가질만한 책을 선별해 소개한다. 큰 관심은 아니지만 작은 관심도 아닌 분야에 대해 간단하게 나마 접할 수 있다.

 

14. 목적에 따라 속독과 정독을 해라.

> 큰 그림 이해는 속독을, 디테일은 정독을.

 

15. 써라.

> 독서용 노트를 따로 만들어 정독시 중요했던 부분을 요약하고 자기만의 언어로 바꿔 적으면 추후에도 기억난다.

 

메타인지 향상 질문 3가지

 

1. 인식 질문 

- 이 일에 어떻게 접근 중인가?

- 이 프로젝트 관련해 어떤 활동 하고 있는가?

- 읽고 있는 내용 이해하지 못할 땐 어떻게 하는가?

- 문제가 생기면 무엇을 하는가?

- 읽고 있는 동안 무슨 생각을 하는가?

 

2. 계획 질문

- 이것은 어떤 일인가?

- 목표는 무엇인가?

- 어떤 정보가 필요한가?

- 일 하는 동안 어떤 문제가 발생 가능하고 어떻게 처리할 것인가?

- 어떤 방법들을 사용 가능한가?

- 어떤 자원을 갖고 있는가?

- 시간이 얼마나 걸릴 것인가?

- 이 작업을 세부 태스크로는 어떻게 나눌 수 있는가?

 

3. 모니터링 질문

- 지금 하고 있는 것이 제대로 되고 있는가?

- 이 일에 대해 이해하지 못하는 부분은 무엇인가?

- 이 일을 어떻게 다르게 할 수 있을까?

- 처음부터 다시 해야하는가?

- 어떻게 하면 지금보다 더 효율적으로 일 처리가 가능할까?

- 이 일에서 통제 가능한 부분과 통제 불가능한 부분은 무엇인가?

- 어떻게 하면 더 많이 더 잘 배울 수 있는가?

- 이 방식이 최선인가?

 

비판적 사고 향상 질문 8가지

- 목적: 이 과제의 목적은?

- 이슈: 이 과제의 구체적 이슈는?

- 정보: 이슈 관련 정보는?

- 추론: 최선의 해결책은 무엇일까?

- 개념: 이슈 생각에 필요한 개념이나 이론은 무엇인가?

- 가정: 이슈 관련해 어떤 가정을 갖는가?

- 시사점: 이슈 해결 시 어떤 결과?

- 관점: 이슈에 대해 어떤 관점

라고는 하지만 실용적이진 않다 생각듦. 

 

생각 발달 단계별 질문

1. 이원론: 답은 하나다. 둘 중 어느 것이 맞느냐. 어떻게 할 수 있냐?

2. 다원론: 어떤 아이디어들이 제시되었느냐, 장단점은 무엇이냐, 근거는 무엇이냐, 고객이 어떻게 생각할까?

3. 상대주의: 여러 의견 중 어떤 것이 낫느냐, 이 상황에선 어떤 해결 방안이 더 적절하다 생각하냐, 이 상황에서 가장 중요하게 고려해야 할 리스크는 무엇인가? 문제 원인 중 상대적으로 더 중요한 것은 무엇인가?

4. 선택과 책임: 의견과 경험을 종합해볼 때 어떤 결론을 내리겠는가? 이 결론을 지지하는 근거는? 이 결론을 어떻게 활용 할 것인가

 

블룸의 인지분류체계에 따른 질문

1. 창조: 독창적인 해결안을 찾을 수 있는가?

2. 평가: 구체적인 기준을 사용해 판단을 내릴 수 있는가?

3. 분석: 개념이나 상황을 요소로 나누고 요소 간 관계를 아는가?

4. 적용: 학습 내용을 새로운 상황에 적용할 수 있는가?

5. 이해: 자신만의 용어로 설명할 수 있는가?

6. 기억: 학습 내용을 기억하는가?

 

리더가 던지면 좋을 질문

1. 이 상황은 이전 상황과 어떤 관련성?

2. 이 상황은 이전 상황과 어떻게 다른가?

3. 문제의 본질은 무엇인가?

4. 이해관계자들의 관심사를 어떻게 통합할 수 있는가

5. 시간이 지남에 따라 문제는 어떻게 변하는가?

6. 그런 변화에 어떻게 우리가 적응할 수 있는가?

7. 다른 회사는 보고 있는데 우리가 놓치는 것이 있는가?

8. 표준 접근법이 일관되게 실패하는 이유는 무엇?

 

토론 업그레이드 질문

- 이 주장과 관련해 어떤 가정을 하고 있는가?

- 가정을 달리하면 결론이나 제안이 어떻게 달라지는가?

 

디딤돌 전략

1. 사건의 발생순서대로 질문하기

2. 단순한 질문에서 복잡한 질문으로 ex) 떠오르는 4차 산업 혁명 키워드는? > 일하는 방식의 변화는? > 직원들의 사고방식 차이는? > 리더는 어떤 역할을?

3. 일반적인 질문에서 구체적인 질문으로 ex) 일반적인 질문으로 의견 수렴 후 구체적으로 들어가기

 

성찰 질문

성찰 일지: 퇴근 15분 전 성찰 일지 쓰면 10일간 22.8% 높은 업무 성과를 보임. 동료들과 공유하면 25%까지 오름.

하버드 1분 페이퍼: 3가지 1) 오늘 학습한 것 중 핵심 포인트는? 2) 강의 중 어떤 질문이 떠올랐는가? 3) 갖고 있던 질문 중 대답을 얻지 못했던 것은?

 

질문포트폴리오

마무리 질문

- 오늘 주제에 대해 아직 가지고 있는 질문은?

- 마지막 질문을 한다면 그것은 무엇일까?

- 오늘 이해하기 가장 까다로웠던 것은?

- 우리가 논의한 것 중 가장 중요한 포인트는 무엇일까?

- 학습한 것 중 꼭 적용하고 싶은 것은 무엇인가?

 

질문일기

- 셀프 질문 스킬 평가 필요. 목적에 부합한 질문? 시기 적절한 질문? 질문 태도? 질문 우선순위?

- 내가 떠오른 질문이나 타인이 했던 좋은 질문들을 리스트화 시켜두고 업데이트하라. 질문 잘하는 사람은 사전에 디자인 해두므로.

 

심리적 안정감 중요

- 멍청한 질문이 세상을 바꿉니다. 어떤 질문이라도 환영합니다.

- 상대방의 입장에서. 지위가 높을 수록 심리적 안전하다 느끼므로 아랫 사람 입장에서 생각할 것.

 

질문 반응

전략 1. 받은 질문에 대해 표현이 어렵다면 어눌하게 답하는 것보다 비슷한 다른 질문에 유창하게 답을 하면 더 좋은 인상을 남긴다. 

전략 2. 향후 10년의 변화나 새로운 규제가 우리 산업에 미칠 영향과 같은 예측이나 추측질문에 응답할 때 조리있게 답하기 어렵다면 추측 의견 내지 말고 핵심 포인트나 메시지를 강조 할 것. ex) 이 산업이 미래에 어떤 향방으로 흐를지 예측할 수 없다. 그러나 ~~~ 방향을 바꿀 큰 기회를 갖고 있다는 것은 확실히 말할 수 있다. 

 

* 공감하는 대목은 모방이 아닌 창조시대이므로 문제 해결형도 중요하나 문제 제기형이 중요하다. 

 

Reference

[1] 임팩트 질문법

 

Rasa 개요

챗봇을 구축하기 위해 사용할 수 있는 라이브러리로는 Rasa가 있고, 한글 버전으로는 Kochat이 있다. 웹 인터페이스 기반으로 코드를 사용하지 않고 쉽게 챗봇을 만들 수 있는 DialogFlow라는 클라우드 서비스가 있지만 간단한 챗봇을 만들 때만 쓸 수 있고, 디테일한 구현이 필요할 땐 Rasa와 같은 오픈소스 라이브러리를 써야한다. Rasa는 크게 Rasa NLU와 Rasa Core로 나뉜다. rasa nlu는 인텐트(intent) 분류와 엔티티(entity) 추출에 사용하는 라이브러리이다. 이를 통해 챗봇 커스터마이징이 가능하고 상상한 거의 모든 종류의 챗봇을 만들 수 있다고 한다. rasa core는 인텐트 추가와 같은 스케일업을 도와주는 라이브러리다. 이 rasa core를 통해 챗봇 응답과 챗봇 동작을 명시 가능하다. 이 명시되는 정보를 액션(Action)이라 하는데, 다르게 말하면 dialouge state에 응답하기 위해 취해야할 행동을 뜻한다. 더하여 rasa core는 과거 히스토리(대화내역)을 기반으로 다음에 이루어져야 할 액션을 예측하는 확률모델을 생성하는 역할을 한다. 

 

Rasa NLU

rasa nlu를 사용하면 학습과 추론을 매우 손쉽게 할 수 있다. 아래 예시 코드와 같이 라이브러리를 제외하면 단 9줄 가량의 코드로도 동작한다. 

from rasa_nlu.training_data import load_data
from rasa_nlu.model import Trainer
from rasa_nlu import config
from rasa_nlu.model import Interpreter

def train_horoscopebot(data_json, config_file, model_dir):
    training_data = load_data(data_json)
    trainer = Trainer(config.load(config_file))
    trainer.train(training_data)
    model_directory = trainer.persist(model_dir, fixed_model_name='horoscopebot')

def predict_intent(text):
    interpreter = Interpreter.load('./models/nlu/default/horoscopebot')
    print(interpreter.parse(text))

 

위 예시 코드는 별자리를 알려주는 태스크를 수행하는 챗봇을 구현하기 위해 사용된 것으로 사용자 입력이 들어오면 아래와 같이 추론 가능하다.

위 결과를 보면 네 개의 사전 정의한 인텐트(greeting, get_horoscope, dob_intent, subscription)중 예측한 인텐트에 대한 신뢰점수가 confidence로 계산되는 것을 확인할 수 있다. 사용자는 별자리를 알고 싶다 말했고, 학습을 진행했던 결과 사용자의 발화(utterance)는 별자리를 묻는 get_horoscope일 확률이 96.12%라는 것을 보여준다. 위와 같이 rasa nlu를 사용해 챗봇 시스템을 학습하고 추론하기 위해서는 간단한 아래 과정을 거친다.

 

1. 챗봇 구현 범위 설정

2. 데이터셋 생성 (data.json)

3. 모델 학습

4. 추론

 

네 가지지만 사실상 챗봇 구현 범위 설정과 데이터셋 생성이 대부분이다. 챗봇 구현 범위 설정이란 모종의 태스크를 수행하는 챗봇을 만든다 가정할 때 사용자가 어떤 종류의 의도를 가질 수 있을지 정하는 것이다. 가령 음식점에서 사용할 수 있는 챗봇을 만든다면 손님은, 주문하거나 결제하거나 화장실 위치를 묻거나 인사하거나 기타 의도를 가진 말을 할 것이다. 이러한 사용자 발화의 범위 설정이 챗봇 시스템 구현의 첫 단계이다. 이후엔 데이터셋 생성이다. rasa nlu를 통해 학습하기 위해서는 json 형태로 데이터를 만들어야 한다. 별자리를 알려주는 태스크를 학습하기 위해 생성한 데이터셋의 예시는 아래와 같다.

 

기본적으로 예상되는 사용자의 발화를 text에 입력하고 그에 따른 라벨이 되는 인텐트의 종류를 입력해준다. 빈 칸의 엔티티는 현재 별자리 운세를 알려주는 태스크에서는 사용되지 않는다. 하지만 간단한 설명을 하자면, 엔티티는 범주를 의미하는 것으로 가령 음식 주문 할 때 A라는 음식 2개를 가져달라고 요청할 수 있다고 가정하면 "음식 종류"라는 엔티티는 "A"라는 값을 가질 것이고 "음식 수량"이란 엔티티는 '2'라는 값을 가질 것이다. 이처럼 하나의 인텐트에는 여러 엔티티를 가질 수 있는 것이 특징이다. 

 

위와 같이 사용자 발화 별 인텐트와 엔티티를 지정하기 위해서 직접 json 포맷으로 작성해줄 수 있지만 쉽게 작성할 수 있는 웹 인터페이스가 있으면 좋을 것이다. rasa에서는 rasa-nlu-trainer라는 웹 인터페이스를 통해 데이터셋을 만드는 작업을 쉽게 할 수 있도록 한다. 일종의 annotation 도구인 것이다. rasa-nlu-trainer는 node.js 기반으로 동작하기 때문에 별도로 node.js를 설치해주어야 하고 sudo npm i -g rasa-nlu-trainer라는 명령어를 통해 설치할 수 있다. 여기서 i는 install이고 g는 global을 뜻한다. 이를 실행하게 되면 다음과 같이 웹으로 쉽게 데이터셋을 만들 수 있도록하는 인터페이스를 제공한다. 

 

 

데이터셋을 직접 만드는데 가장 시간이 많이 걸릴 것이다. 가급적이면 공개되어 있는 것을 가져올 수 있겠지만 내가 원하는 특정 태스크를 수행하기 위해서는 대부분 커스터마이징이 필요할 것이고 이에 따른 데이터는 직접 구축하는 경우가 많을 것이다. 

 

Rasa Core

rasa nlu를 통해 예측된 intent에 따라 챗봇의 응답과 동작이 이루어질 것이다. 이 때 사용하는 것이 rasa core이다. rasa core를 통해 챗봇 응답과 챗봇 동작을 명시할 수 있다. 챗봇에서 동작을 'Action'이라 한다. rasa core를 사용하면 과거의 대화내역을 기반으로 챗봇이 해야할 다음 Action을 예측하는 모델을 생성할 수 있다. 이를 위해 가장 먼저 도메인 파일이라 불리는 것을 만들어 주어야 한다. 도메인 파일이란 쉽게 말해 어떤 주제 속에서 대화가 이뤄지는지 정의하는 파일이다. 음식점에서 사용할 챗봇이면 도메인이 음식점이 되는 것과 같다. 이 도메인 파일은 크게 5가지 내용을 포함 해야한다. 1. 인텐트 2. 엔티티 3. 슬롯 4. 템플릿 5. 액션이다. 도메인 파일은 yml 확장자를 가지며 아래와 같은 형태로 정의할 수 있다.

 

 

1. 인텐트는 언급했듯 발화의 의도를 말한다.

2. 엔티티는 발화 속 캐치해야 할 핵심 키워드를 뜻하고

3. 슬롯은 키워드가 채워질 공간을 말한다. 별자리 별 운세를 알려주는 태스크에서는 월/일을 알아야 하므로 MM, DD같은 슬롯이 정의될 수 있다. 

4. 템플릿은 말 그대로 인텐트에 대한 기본 응답을 뜻한다. 여러 개의 기본 응답을 만들어 랜덤으로 사용한다면 다양하기에 사용자 경험에 있어 친근감을 줄 수 있는 요소가 될 것이다.

5. 액션은 사용자 발화에 따라 어떤 행동을 취할 수 있을지를 정의하는 것이다. 

 

이렇게 도메인 파일 내에 5개의 요소를 넣어 yml 파일로 정의했으면 다음으론 거의 끝으로 스토리 파일을 만들어 주어야 한다.

스토리 파일이란 대화가 어떤 시나리오로 흐를 수 있는지 정의한 파일이다. 파일은 마크다운 확장자로 만들어준다. 예시는 아래와 같다.

 

간단하다. 첫 번째 스토리는 챗봇이 사용자에게 인사하고 별자리가 무엇이냐고 물어본다. 이후 Capricorn(마갈궁자리)라는 응답을 받아 slot에 채워주고 현재 별자리 운세를 가져와서 보내주고 이 서비스를 구독하겠는지 묻고 끝나는 상황이다. 두 번째 스토리는 첫 번째와 달리 챗봇이 인사한 다음 사용자가 바로 별자리를 알려주고 cancer(게자리)라는 정보를 slot에 채우고 현재 별자리 운세를 가져와 사용자에게 전달하고 이 서비스를 구독하겠는지 묻고 사용자가 구독한다는(True) 발화를 캐치한 다음 구독자 목록에 추가하는 프로세스로 이뤄지는 것을 알 수 있다. 

 

사용자는 미리 정의된 템플릿에 따라 액션을 취할 수 있지만 이는 정적이다. 예컨데 운세를 가져오거나 구독자 추가와 같은 동적으로 이뤄저야 하는 것은 별도의 API를 사용해 외부 데이터베이스와 연결될 필요가 있다. 이 때는 Custom 액션이라 하여 파이썬 스크립트를 별도로 만들어 주어야 한다. 간단한 코드 예시는 다음과 같다.

 

class GetTodaysHoroscope(Action):
    def name(self):
        return "get_todays_horoscope"

    def run(self, dispatcher, tracker, domain):
        user_horoscope_sign = tracker.get_slot('horoscope_sign')
        base_url = http://horoscope-api.herokuapp.com/horoscope/{day}/{sign}
        url = base_url.format(**{'day':"today", 'sign':user_horoscope_sign})
        # http://horoscope-api.herokuapp.com/horoscope/today/capricorn
        res = requests.get(url)
        todays_horoscope = res.json()['horoscope']
        response = "Your today's horoscope:\n{}".format(todays_horoscope)

        dispatcher.utter_message(response)
        return [SlotSet("horoscope_sign", user_horoscope_sign)]

 

오늘 별자리 별 운세를 가져오기 위해 GetTodaysHoroscope라는 클래스를 만들어주고 내부에 run이라는 비즈니스 로직을 작성해줄 수 있다. run의 매개변수로 크게 dispatcher, tracker, domain이 들어가는 것을 알 수 있다. 먼저 tracker는 현재 상태 추적기로 슬롯에 접근해 값을 가져올 수 있고 이외에도 최근 사용자의 발화 정보 내역들에 접근할 수 있다. dispatcher는 별자리 정보를 기반으로 운세를 알려주는 API 호출 결과를 사용자에게 전달하는 역할을 한다. domain은 글자 그대로 이 챗봇의 도메인을 의미한다. 마지막으로 보면 SlotSet이 있는데 이는 추후 사용자 발화 정보를 히스토리로 활용하기 위해 슬롯이라는 공간에 저장하는 것이다. 

 

마지막으로 학습은 아래와 같이 간단한 로직으로 이루어진다.

""" Training a chatbot agent """
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from rasa_core import utils
from rasa_core.agent import Agent
from rasa_core.policies.keras_policy import KerasPolicy
from rasa_core.policies.memoization import MemoizationPolicy
from rasa_core.policies.sklearn_policy import SklearnPolicy
import warnings
import ruamel
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)

if __name__ == "__main__":
    utils.configure_colored_logging(loglevel="DEBUG")
    training_data_file = "./data/stories.md"
    model_path = "./models/dialogue"
    agent = Agent("horoscope_domain.yml", policies=[MemoizationPolicy(), KerasPolicy(), SklearnPolicy()])
    training_data = agent.load_data(training_data_file)
    agent.train(
        training_data,
        augmentation_factor=50,
        epochs=500,
        batch_size=10,
        validation_split=0.2
    )
    agent.persist(model_path)

 

main을 살펴보면 1. 로깅 설정 2.스토리 파일 경로 설정 3. 모델 저장 경로 설정 4. 다음 액션을 가져오는 에이전트 세팅 5~6. 학습, 마지막으로 에이전트 오브젝트(모델)를 지정된 경로에 저장하여 재사용하는 로직으로 구성된다. 이렇게 해주면 에이전트 모델 학습이 이루어진다. rasa nlu를 통해 추론한 사용자 발화 의도를 기반으로 rasa core를 통해 학습한 에이전트로 다음 챗봇 Action을 수행할 수 있다. 

 

Reference

[1] 파이썬으로 챗봇 만들기 

엔트로피(Entropy)

물리학에서 무질서도라는 개념으로 사용된 Entropy는 확률통계학에서 확률분포의 불확실성을 나타내는 척도이다. 이 불확실성의 척도는 확률분포에 대한 정보량의 기대값으로 표현한다. 하나의 확률분포에 대한 불확실성은 entropy로 계산하고, 두 확률분포 간의 불확실성은 cross entropy를 통해 계산한다. entropy와 cross entropy는 다음과 같이 정의된다.

$\displaystyle H(x) = -\sum_{i=1}^n p(x_i)\log(p(x_i))$

위 식에서 확률값인 $p(x_i)$의 총합은 1이기 때문에 수식은 $\displaystyle H(x) = -\sum_{i=1}^n \log p(x_i)$와도 같다. 간단한 예시인 동전과 주사위를 통해 먼저 entropy를 계산해보자. 동전의 경우 확률이 $\displaystyle 1 \over 2$인 이산확률분포를 가지며 주사위의 경우 확률이 $\displaystyle 1 \over 6$인 확률분포를 가진다. 동전과 주사위를 던지는 상황에 대한 entropy는 각각 다음과 같이 계산된다.

$\displaystyle H(x) = - \left({1\over 2}\log{1\over 2} + {1\over 2}\log{1\over 2}\right) = 0.3010$

$\displaystyle H(x) = -\left({1\over 6}\log {1\over 6} + {1\over 6}\log {1\over 6} + {1\over 6}\log {1\over 6} + {1\over 6}\log {1\over 6} + {1\over 6}\log {1\over 6} + {1\over 6}\log {1\over 6}\right) = 0.7782$


위 예시를 통해 알 수 있는 것은 확률변수가 다양하게 나올 수 있는 경우, 즉 확률에 대한 불확실성이 큰 경우 entropy가 더 크다는 것이다.

 

크로스 엔트로피(Cross Entropy)

entropy가 확률분포 하나에 대한 불확실성을 나타내는 척도라면 cross entropy는 확률분포 두 개에 대한 불확실성을 나타내는 척도다. 즉 cross entpry는 두 확률분포 간의 차이를 구하기 위해 사용한다. 머신러닝에서 실제 데이터의 확률분포와 모델이 예측한 확률분포 간의 차이를 구하는 것이다. Cross Entropy는 다음과 같은 함수로 정의된다.

$\displaystyle H_{p,q}(x) = -\sum_{i=1}^n p(x_i)\log(q(x_i))$

위 수식에서 $p(x)$는 실제 데이터의 확률 분포를 의미하며 $q(x)$는 모델이 예측한 확률분포 분포를 의미한다. 이를 활용하여 머신러닝에선 손실함수로 사용할 수 있다. 데이터셋으로부터 실제 확률분포인 $p(x)$를 계산할 수 있고 만든 모델로 예측을 통해 $q(x)$를 알 수 있기 때문이다. 이 둘 간의 차이를 손실로 두어 이 손실이 줄어드는 방향으로 학습하는 것이다.

간단한 예시를 통해 크로스 엔트로피를 계산해보자. 만약 A, B 두사람이 있고 한 개의 상자안에 있는 RGB 색상을 갖는 공이 $0.8, 0.1, 0.1$의 비율로 들어가 있다고 가정하자. 하지만 A는 직감적으로 $0.6, 0.2, 0.2$의 비율로 들어 있을 것 같다 예측했고 B는 $0.4, 0.3, 0.3$으로 들어 있을 것 같다 예측할 경우 cross entropy는 다음과 같이 계산된다.

$\displaystyle H_{p, q}(x) = - \left({8\over 10} \log({6\over 10}) + {1\over 10} \log({2 \over 10}) + {1\over 10}\log({2 \over 10})\right) = 0.3173 $

$\displaystyle H_{p, q}(x) = - \left({8\over 10} \log({4\over 10}) + {1\over 10} \log({3 \over 10}) + {1\over 10}\log({3 \over 10})\right) = 0.4230$


A의 예측은 0.3173이고 B의 예측은 0.4230이다. 즉 예측과 멀어지면 멀어질수록 cross entropy가 증가하는 것이다. 이러한 수식을 이용해 ML/DL에서 cross entropy 값이 줄어드는(불확실성이 줄어드는) 방향으로 학습을 진행한다.


KL Divergence란?

쿨백-라이블러 발산(Kullback-Leibler Divergence, KLD)은 두 확률분포 간의 차이를 측정하는 함수다. 두 확률분포 간의 차이는 $H_{p, q}(x) - H(x)$로 계산한다. 즉 크로스 엔트로피에서 엔트로피를 빼준 값이 KL Divergence 값이다.

$\displaystyle H(x) = -\sum_{i=1}^n p(x_i)\log(p(x_i))$
$\displaystyle H_{p,q}(x) = -\sum_{i=1}^n p(x_i)\log(q(x_i))$

$\displaystyle KL(p || q) = H_{p, q}(x) - H(x) = \sum_{i=1}^n p(x_i) \log {p(x_i)\over q(x_i)}$


두 확률분포는 $p(x), q(x)$로 표현하며 각각 사전 확률분포, 사후 확률분포이다. 달리 말해 $p(x)$는 실제 확률분포고 $q(x)$는 예측 확률분포이다. KL Divergence의 수식은 다음과 같이 이산형과 연속형으로 나뉘어 정의된다.

$KL(p || q) = \begin{cases} \displaystyle \sum_{i=1}^n p(x_i) \log{p(x_i) \over q(x_i)} \mbox{(이산형)} \\ \int p(x) \log{p(x) \over q(x)}dx \mbox{(연속형)} \end{cases}$

 

예측 확률분포인 $q(x)$가 실제 확률분포인 $p(x)$에 가까이갈수록 KL Divergence 값은 0에 가까워진다. 그렇다면 Cross Entropy와 KL Divergence의 차이점은 무엇일까? 둘 다 공통적으로 두 확률분포 간의 차이를 측정하는 척도이자 함수이다. KL Divergence를 최소화하는 것은 Cross Entropy를 최소화하는 것과 같다. 하지만 다른 점은 KL- Divergence 내의 $p(x)\log p(x)$는 실제 확률분포로서 알 수 없는 분포다. $H(x)$를 모르므로 KL-Divergence를 손실함수로 적용할 수 없다. 따라서 이를 제외하고 남은 $-p(x)\log q(x)$인 cross entropy를 사용한다.

 

 

Reference

[1] [머신러닝] 크로스 엔트로피(cross entropy) (rcchun)
[2] Cross-entropy 의 이해: 정보이론과의 관계 (DEEPPLAY)
[3] CrossEntropy와 KL-divergence (Jeff_Kang)

마르코프 체인 정의

마르코프 체인(Markov Chain)은 마르코프 성질을 지닌 이산확률과정이다. 마르코프 성질이란 $n+1$회의 상태(state)는 $n$회의 상태나 그 이전 $n-1, n-2, \dots$의 상태에 의해 결정되는 것이다. 달리 말해 과거 상태가 현재/미래 상태에 영향을 미치는 성질이다. 이산확률과정이란 시간의 진행에 대해 확률적인 변화를 가지는 구조를 의미한다. 이 마르코프 체인은 때때로 단순하지만 강력한 효과를 발휘하기에 사용한다. 실제로 예측을 위해선 많은 변수과 모델링을 거쳐야 하지만 마르코프 체인은 이런 비용을 줄여주기 때문이다.

 

마르코프 체인 예시

마르코프 성질 여부에 대한 흔한 예시로는 동전 앞뒤 예측과 날씨예측이 있다. 동전 앞뒤를 예측하는 것은 독립시행이기 때문에 n번째 상태가 n+1번째 상태에 영향을 주지 않으므로 마르코프 성질이 없다. 반면 날씨 예측과 같이 직관적으로 오늘 날씨에 의해 내일 날씨가 결정될 수 있으므로 마르코프 성질이 있다고 할 수 있다. 만약 오늘을 기반으로 하루 뒤를 예측한다면 1차 마코프 모델이라하고 이틀 뒤를 예측한다면 2차 마코프 모델이라 한다. 

 

 

마르코프 체인 활용

마르코프 체인은 주로 결합확률분포(Joint Probability Distribution)에 사용된다. 예를 들어 확률 변수 $X_1, X_2, \dots, X_n$이 있다고 가정하면 일반적으로 이 확률변수들의 결합확률분포는 다음과 같이 계산할 수 있다.

 

$P(X_1, X_2, \dots, X_n) = P(X_1) \times P(X_2|X_1) \times P(X_3|X_2,X_1) \times \dots \times P(X_n|X_{n-1}, X_{n-2}, \dots, X_1)$

 

하지만 마르코프 성질을 이용하면 위 보다 더 단순한 계산을 통해 결합확률분포를 구할 수 있다.

만약 어떠한 상태의 시점이 $t$고, 확률분포가 마르코프 성질을 따른다면 

 

$P(X_{t+1}|X_t, X_{t-1}, \dots, X_2, X_1) = P(X_{t+1}|X_t)$

 

로 단순화 할 수 있고 일반화를 적용하면 이전에 결합확률분포의 계산을 다음과 같이 단순화 가능하다.

 

$P(X_1, X_2, \dots, X_n) = P(X_1) \times P(X_2|X_1) \times P(X_3|X_2) \times P(X_4|X_3) \times \dots \times P(X_n|X_{n-1})$

 

이러한 마르코프 체인은 주로 베이지안 통계학이나 강화학습에 사용되며, MCMC(Markov Chain Monte Carlo)와도 연결되어 사용된다.

 

마르코프 모델

마르코프 모델이란 마르코프 체인을 기반으로 만든 확률 모델이다. 아래 날씨 예제를 통해 내일 날씨를 예측하는 마르코프 모델을 만들어보자.  마르코프 모델을 만들기 위해선 가장 먼저 각 상태를 정의해야 한다. 그리고 이 상태는 상태 전이 확률로 나타낸다. 상태 전이 확률이란 어떤 상태에서 다른 상태로 이동할 확률을 의미한다. 상태 전이 확률을 행렬로 표현한 것을 전이 행렬(Transition Matrix)라고 한다.

 

전이 행렬(Transition Matrix)

 

이 전이 행렬을 해석하자면 만약 오늘 날씨가 Sunny라면 내일 Suuny일 확률이 0.7, Rainy일 확률이 0.2, Cloudy일 확률이 0.1이라 예측한다. 이를 기반으로 오늘이 어떤 특정 날씨일 때 내일이 어떤 특정 날씨일 확률을 구하기 위해서는 위 전이 행렬이 $T$라고할 때 $T \times T$처럼 곱해주면 된다. 만약 오늘 날씨를 기반으로 이틀 뒤 날씨를 예측하고 싶다면 $T \times T \times T$를 계산하면 된다. 이렇게 반복적으로 전이행렬을 곱해줌으로써 일련의 예측을 함께 연결한다면 이를 마르코프 체인이라 한다.

 

참고로 전이 행렬을 거듭 곱하다보면 더 이상 전이 행렬의 값이 변하지 않고 수렴하는 상태가 오는데 이를 안정 상태(Steady State)라고 한다. 즉 행렬 $T$는 거듭곱해 어떤 행렬 $M$이 되지만 수렴한 뒤에는 $MT = T$가 된다. 또 전이 행렬은 도식화 가능하다. 이 도식화한 것을 상태 전이도(State Transition Diagram)라고 하며 아래와 같다.

 

상태 전이도 (State Transition Diagram)

 

전이 행렬 또는 상태 전이도를 기반으로 마르코프 모델을 만들어 날씨 예측을 할 수 있다. 아래는 10일간 날씨 예측을 할 수 있는 간단한 파이썬 스크립트다.

 

import numpy as np

transition_matrix = np.atleast_2d([[0.7, 0.2, 0.1],
                                   [0.1, 0.6, 0.3],
                                   [0.2, 0.5, 0.3]])

possible_states = ["sunny", "rainy", "cloudy"]
start_state = "sunny"
num = 10
index_dict = {}
future_state = []

for index in range(len(possible_states)): # {'sunny': 0, 'rainy': 1, 'cloudy': 2}
    index_dict[possible_states[index]] = index

for _ in range(num):
    new_state = np.random.choice(possible_states, p=transition_matrix[index_dict[start_state], :])
    future_state.append(new_state)

print (future_state) 
"""
> ['rainy', 'sunny', 'rainy', 'sunny', 'sunny', 'rainy', 'cloudy', 'sunny', 'sunny', 'sunny']
It will be different whenever you run it. because I used np.random.choice
"""

 

 

용어 정리

마르코프 체인(Markov Chain): 상태 전이 확률을 기반으로 일련의 예측을 연결한 것

마르코프 성질(Markov Property): $n+1$ state는 $n$ state에 의해 결정되는 것처럼 상태들 간의 인과성이 있는 성질

이산확률 과정(Discrete Stochastic Process): 시간 흐름에 따라 확률적인 변화를 가지는 구조. 특히 시간 흐름이 이산적일때를 의미

연속확률 과정(Continous Stochastic Process): 시간 흐름에 따라 확률적인 변화를 가지는 구조. 특히 시간 흐름이 연속적일 때를 의미

마르코프 모델(Markov Model): 미래 상태에 대한 확률 값이 과거에만 종속된 모델

상태 전이 확률(State Transition Probability): 어떤 상태에서 다른 상태로 이동할 확률

전이 행렬(Transition Matrix): 전이 확률을 행렬 형태로 나타낸 것

상태 전이도(State Transition Diagram): 상태 전이 확률을 정리하여 만든 도식

안정 상태(Steady State): 전이 행렬을 거듭 곱해도 더 이상 변하지 않는 상태

정적분포(Stationary Distribution): 전이 행렬이 안정 상태일 때 갖는 확률분포

상태 공간(State Space): $0, 1, 2, \dots, n-1, n$ 시점에서의 확률과정의 상태들의 집합

 

Reference

[1] Markov Chains

[2] Markov Chain Text Generation – Part 1 (KEN GRAHAM)

[3] 마르코프 체인 (Markov Chain) 정의 및 예시 (Data to Impact)

 

+ Recent posts