2021년 12월 9일 목요일

미국 하우스 이사하기

이 글은 미국에서 거주할 때, 하우스에서 이사하는 과정을 간략히 정리하고, 공유한 것이다. 

미국에서 거주하며, 이사하거나 할 때, 살던 집을 청소, 짐 정리, 수리, 유틸리티 서비스 정지, 학교 이동 처리 등을 해야 한다.


아래는 이 경우, 해야할 일들이다. 모든 과정은 최소 2주가 소요된다. 가급적 한달전부터 준비하는 것이 좋다. 아래의 종료 및 취소 요청 예약은 대부분 전화나 대면 방문으로 처리해야 한다. 관공서로 부터 서류를 받아야 경우는 한달 정도 여유를 두고 일처리를 한다(예. 애완동물 검역 서류 처리는 2달 이상 걸림). 

  1. 이사짐 처리 업체 견적 및 예약
  2. 렌트일 경우, 집 관리자나 주인에게 종료 일자 알리기
  3. 불필요한 대형 가구, 가전 등 물건은 중고 팔기(예. Offerup, 페이스북 커뮤니티, Garage Sale, Donation, E-Bay, 등)
  4. 대형 쓰래기(예. 가구, 매트릭스, 소파 등)는 미리 지역내 Trash 픽업 서비스 예약하기(예. Republic service 등)
  5. 소형 중고물품 중고 판매 및 게러지 세일(커뮤니티 세일)
  6. 하우스 손상된 부분있으면 원상복귀하기(예. 실내외 페인트, 바닥 및 화장실 청소, 손상 부분 교체 등). 수리 예약 및 처리하기
  7. 이사 후 임시 거처, 숙소 렌트하기
  8. 유틸리티(예. 물, 전기 등) 종료 처리하기
  9. 각종 휴대폰, 통신, 보험 종료 처리하기
  10. 인터넷 및 전화 등 각종 서비스 종료 처리하기
  11. 집 관리자 등에게 이사된 짐에 오는 우편물을 임시로 받을 주소 알려주기 및 각종 서비스 우편물 받는 주소 변경
  12. 아이 학교 전학 카운슬러에게 요청하기
  13. 이사 및 이동에 필요한 차량 교통편 예약, 렌트하기
  14. 항공편 이동일 경우, 미리 PCR 예약 및 테스트(출발 전 3일내 검사 수행)
  15. 이사 짐싸기
  16. 주변 이웃들과 인사하기
  17. HOA 종료 연락하기
  18. 하우스 관리자와 마지막으로 손상 여부 등 Work Through 체크하기
미국은 아파트와 하우스의 차이가 크다. 각자 장단점이 있으나, 하우스가 손이 더 많이 가는 편이다. 이사하기 전에는 모든 손상된 부분이 고쳐져야 하며, 관리자와 마지막 체크를 해야 한다. 각자 일정이 있기 때문에, 사람을 통해야 되는 부분은 모두 전화 등으로 사전 예약을 해야 함에 주의한다.

2021년 11월 23일 화요일

유명한 오픈소스 WebGL 프레임웍 소개

이 글은 유명한 오픈소스 WebGL 프레임웍을 소개한다.

2021년 11월 22일 월요일

HTML5 웹기반 인터렉티브 그래픽 개발 도구 Konva 소개

이 글은 konva를 이용한 HTML5 기반 인터렉티브 그래픽 개발 도구를 간략히 이야기한다.

콘바(Konva)는 웹 기반 인터렉티브 그래픽을 지원하는 HTML5 Canvas JavaScript 프레임워크이다. 콘바는 고성능 애니메이션, 화면 전환, 그래픽 노드 오버레이, 계층화, 필터링, 캐싱, 데스크톱 및 모바일 애플리케이션에 대한 이벤트 처리 등을 가능하게 한다. 이를 이용해, 딥러닝 라벨링 도구 같은 웹 기반 그래픽 편집기, 게임 등을 쉽게 개발할 수 있다.
콘바 기반 웹 그래픽 설명 서비스

개념
콘바는 가상 무대에 객체를 그리고, 이벤트 리스너를 추가한 후, 이동하고, 크기를 조정하거나, 다른 모양과 독립적으로 회전하는 등 애니메이션을 지원한다. 

콘바는 다음과 같은 계층적 구조를 가진다.

설치
node.js 설치 후 다음 명령을 명령창에서 입력한다.
npm install konva

혹은 html 이나 자바 스크립트 파일에 아래 태그를 포함한다.
<script src="https://unpkg.com/konva@8/konva.min.js"></script>

사용 방법
콘바는 웹 기반 그래픽 처리를 위해 여기와 같은 다양한 API를 제공한다.

이러한 기능을 이용해, 다음 그래픽을 생성하는 예제를 확인해 보자.

다음 스크립트를 html로 저장해 크롬 등에서 실행해보자. 코드 명령 실행 순서는 위에서 소개한 개념도와 같이, 스테이지를 만든 후, 레이어에 툴팁, 라벨 등 도형을 추가한다. 콘바는 이와 같이 매우 직관적으로 동작되므로, 이해가 그리 어렵지 않다.
<!DOCTYPE html>
<html>
  <head>
    <script src="https://unpkg.com/konva@8.3.0/konva.min.js"></script>
    <meta charset="utf-8" />
    <title>Konva Label Demo</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        background-color: #f0f0f0;
      }
    </style>
  </head>

  <body>
    <div id="container"></div>
    <script>
      var stage = new Konva.Stage({
        container: 'container',
        width: window.innerWidth,
        height: window.innerHeight,
      });

      var layer = new Konva.Layer();

      // tooltip
      var tooltip = new Konva.Label({
        x: 170,
        y: 75,
        opacity: 0.75,
      });

      tooltip.add(
        new Konva.Tag({
          fill: 'black',
          pointerDirection: 'down',
          pointerWidth: 10,
          pointerHeight: 10,
          lineJoin: 'round',
          shadowColor: 'black',
          shadowBlur: 10,
          shadowOffsetX: 10,
          shadowOffsetY: 10,
          shadowOpacity: 0.5,
        })
      );

      tooltip.add(
        new Konva.Text({
          text: 'Tooltip pointing down',
          fontFamily: 'Calibri',
          fontSize: 18,
          padding: 5,
          fill: 'white',
        })
      );

      // label with left pointer
      var labelLeft = new Konva.Label({
        x: 20,
        y: 130,
        opacity: 0.75,
      });

      labelLeft.add(
        new Konva.Tag({
          fill: 'green',
          pointerDirection: 'left',
          pointerWidth: 20,
          pointerHeight: 28,
          lineJoin: 'round',
        })
      );

      labelLeft.add(
        new Konva.Text({
          text: 'Label pointing left',
          fontFamily: 'Calibri',
          fontSize: 18,
          padding: 5,
          fill: 'white',
        })
      );

      // simple label
      var simpleLabel = new Konva.Label({
        x: 180,
        y: 150,
        opacity: 0.75,
      });

      simpleLabel.add(
        new Konva.Tag({
          fill: 'yellow',
        })
      );

      simpleLabel.add(
        new Konva.Text({
          text: 'Simple label',
          fontFamily: 'Calibri',
          fontSize: 18,
          padding: 5,
          fill: 'black',
        })
      );

      // add the labels to layer
      layer.add(tooltip).add(labelLeft).add(simpleLabel);

      // add the layer to the stage
      stage.add(layer);
    </script>
  </body>
</html>

Konva 기반 딥러닝 라벨링 도구 개발 사례
콘바를 이용하면, 딥러닝 학습 데이터 생성에 필요한 라벨링 도구를 웹기반으로 쉽게 개발할 수 있다. 다음은 관련 개발 사례이다.

마무리
이 글은 웹기반 인터렉티브 그래픽 에디터, 애니메이션 등 다양한 곳에 사용되는 콘바를 간략히 소개하였다. 콘바는 매우 많은 데모를 제공하여, 개발자가 손쉽게 서비스를 개발할 수 있도록 지원한다. 관심이 있다면, 아래 링크를 참고하길 바란다.

Reference

NVIDIA 아이작심(Isaac Sim) 기반 대규모 병렬 로보틱스 학습 기술 소개, 설치, 사용 및 피지컬AI모델 학습방법

이 글은 NVIDIA 아아작심 기반 대규모 병렬 로보틱스 학습 시뮬레이션, 사용법 및 모델 학습 방법을 간략히 소개한다. 아울러, 혼동스러운 용어인 물리AI와 차이점도 나눔한다.

강화학습 기반 로봇 물리 AI 시뮬레이션

이 기술은 단일 GPU에서 대규모 병렬 처리를 사용해, 로봇을 4분 이내에 평평한 지형에서, 20분 내에 고르지 않은 지형도 걷을 수 있게 학습한다. ETH Zurich와 NVIDIA의 연구팀은 Massively Parallel Deep Reinforcement Learning을 사용하여 몇 분 안에 걷는 법(Learn to Walk in Minutes)이라는 논문을 릴리즈했다. 

이 기술은 이전 방법과 비교할 때 훈련 시간을 몇 배나 줄일 수 있다. 현재 강화 학습 접근 방식은 데이터 수집과 정책 업데이트로 구성된다. PCIe(Peripheral Component Interconnect Express)를 통한 데이터 전송은 GPU 처리 시간보다 50배 느릴 수 있다. 이러한 문제를 극복하기 위해, 제안된 DRL 알고리즘은 PPO(Proximal Policy Optimization) 알고리즘을 기반으로 하며 모든 데이터를 GPU에 저장하도록 설계되었다. 

연구팀은 단일 워크스테이션 GPU에서 Isaac Gym 물리 시뮬레이션 환경을 기반으로한 DRL 알고리즘으로 로봇을 훈련하는 여러 평가 실험을 수행했다. 실험 결과 흥미로운 관찰 결과가 나왔습니다. 시뮬레이션 및 배치 실험을 수행하여 최대 0.2m(가장 어려운 계단 난이도)까지 로봇이 오르내리는 성공률이 거의 100%에 달했다. 

전반적으로 이 연구는 수천 대의 로봇이 병렬로 걷도록 가르치는 데 필요한 훈련 시간을 줄일 수 있음을 보여준다.

아아직심 설치 및 사용법

아이작심은 엔비디아 옴니버스 플랫폼 상에서 동작한다. 실행을 위해선는 최소한 24GB VRAM이 장착된 4090 이상 하이엔드급 GPU 서버가 필요하다. 

nvidia 드라이버 설치는 다음과 같다(이미 드라이버 있을 겨우 생략). 
1. 단계 우분투 터미널을 열고 sudo apt-get purge "nvidia" -y 명령어로 꼬여있는 기존 드라이버를 삭제
2. sudo apt-get purge "libnvidia" -y 명령어를 입력하여 남은 관련 라이브러리도 삭제
3. sudo apt-get autoremove -y 명령어로 불필요한 잔여 패키지를 모두 청소
4. sudo apt-get update 명령어로 우분투 패키지 목록을 최신 상태로 새로고침
5. sudo apt-get install nvidia-driver-550 -y 명령어를 입력하여 아이작 심 구동에 안정적인 550 버전 드라이버를 설치
6. sudo reboot 명령어를 입력하여 컴퓨터를 재부팅
7. 재부팅 후 터미널에서 nvidia-smi 명령어를 쳐서 그래픽카드가 정상적으로 인식되는지 확인

아이작심 설치는 다음과 같다.
1. 다음 명령을 터미널에서 실행
sudo apt update
sudo apt install libfuse2
sudo apt install libjemalloc2


3. 다운로드 폴더에서 다음 명령 실행
mkdir ~/isaac-sim
unzip isaac-sim-*.zip -d ~/isaac-sim

4. 다음 명령을 실행해 아이작심 실행
echo 'alias isaac="cd ~/isaac-sim && LD_LIBRARY_PATH=\"\" LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./isaac-sim.sh"' >> ~/.bashrc && source ~/.bashrc

다음은 아이작심 사용 방법이다. 강화학습 예제 실행 순서는 다음과 같다. 
1. 상단 메뉴에서 Window를 클릭. 
2. Examples 메뉴 안의 Robotics Examples를 클릭 
3. 화면 하단에 새로 열린 Robotics Examples 탭을 선택 
4. 폴더 목록에서 POLICY를 열고 Quadruped를 선택 
5. 화면 우측 하단의 LOAD 버튼을 눌러 강화학습 예제 환경을 로딩 
6. 화면 좌측 도구 모음에서 재생 버튼을 눌러 물리 엔진을 가동
7. 화면 우측 하단의 START POLICY 버튼을 눌러 인공지능 제어를 시작 
8. 화면의 안내에 따라 키보드 방향키를 사용하여 로봇을 움직임. 학습된 모델 기반으로 로봇 동작됨

Isaac Sim

렌더링 모드로 인해 속도가 많이 느리면 다음 설정 변경한다. 
1. 뷰포트 화면 좌측 상단에 있는 RTX Real-Time 글자를 클릭
2. 드롭다운 메뉴에서 Unlit 항목을 선택하여 빛과 그림자 연산을 완전히 끔 
3. 더 가벼운 환경이 필요할 경우 Wireframe 항목을 선택하여 객체의 윤곽선만 표시
4. 뷰포트 화면 우측 상단의 톱니바퀴 아이콘을 클릭 
5. Resolution Scale 값을 기본 1.0에서 0.5 이하로 낮추어 렌더링 해상도를 줄임
6. 화면 우측 Render Settings 창에서 Ray Tracing 탭을 선택
7. Shadows 항목을 찾아 체크를 해제하여 그림자 연산을 종료 
8. Global Illumination 항목을 찾아 체크를 해제하여 주변광 연산을 종료

화면은 다음과 같이 조작한다.
1. 화면 회전은 알트 키와 마우스 왼쪽 버튼을 동시에 누른 채로 움직임
2. 화면 평행 이동은 알트 키와 마우스 가운데 휠 버튼을 누른 채로 움직임
3. 화면 줌인 및 줌아웃은 마우스 휠을 굴리거나 알트 키와 오른쪽 버튼을 누른 채 동작
4. 선택한 물체를 화면 중앙으로 다시 불러오려면 키보드 F 키를 누름 

강화학습 실행 방법

강화학습하는 방법은 다음과 같다. 
1. 단계 터미널을 열고 cd ~ 명령어로 홈 디렉토리로 이동
2. git clone https://github.com/isaac-sim/IsaacLab.git 명령어로 강화학습 도구를 다운로드
3. cd IsaacLab 명령어로 폴더에 진입
4. export ISAACSIM_PATH=~/isaac-sim 명령어로 설치된 경로를 연결
5. ./isaaclab.sh --install 명령어를 입력하여 파이썬 환경 세팅을 완료
6. ./isaaclab.sh -p source/standalone/workflows/rsl_rl/train.py --task=Isaac-Velocity-Flat-Anymal-C-v0 --num_envs=4096 --headless 명령어로 훈련을 시작
7. 훈련이 완료되면 ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task=Isaac-Velocity-Flat-Anymal-C-v0 명령어로 시뮬레이션 결과를 확인


학습이 완료된 인공지능 모델 파일 자체를 USD 파일로 변환하여 저장하는 것은 아니다.
USD 파일은 로봇의 3D 외형과 물리 속성만을 담고 있으며 훈련된 모델 파일은 파이썬 스크립트를 통해 USD 로봇을 외부에서 조종하는 방식으로 작동한다. 완성된 인공지능 모델을 USD 로봇 환경에 연결하여 배포하는 공식 튜토리얼 링크는 다음과 같다.
아이작 랩을 활용하여 파이썬으로 로봇을 처음부터 훈련시키는 엔비디아 공식 튜토리얼 링크는 다음과 같다.

강화학습 모델 개발 방법

강화학습 하기 전에 스팟 로봇의 구동 관절 구조와 모델 출력 Tensor 매핑 메커니즘을 확인해야 한다. 
보스턴 다이내믹스의 스팟 로봇은 4개의 다리에 각각 3개씩 총 12개의 구동 모터 관절을 가지고 있다.  각 다리는 몸통과 좌우로 연결되는 Hip 관절, 허벅지를 앞뒤로 움직이는 Upper Leg 관절, 무릎을 굽히는 Lower Leg 관절의 3자유도 구조로 이루어집니다. 

Isaac Sim 내부에서 각 모터는 Front Left, Front Right, Rear Left, Rear Right 등 4개 다리의 위치와 세부 관절 이름의 조합으로 고유한 순서를 가진다. PPO 알고리즘의 Actor 신경망이 계산해 내는 최종 행동 출력 Tensor는 1차원 배열 형태의 크기가 12인 실수형 벡터 포맷이다. 출력 Tensor의 인덱스 0번부터 11번까지의 출력값은 USD 파일에 정의된 로봇의 12개 관절 트리 순서와 정확히 일대일로 매핑된다.

일반적으로 Tensor의 0번부터 2번 인덱스는 왼쪽 앞다리의 세 관절에 할당되고 9번부터 11번 인덱스는 오른쪽 뒷다리의 세 관절에 할당되는 방식이다. 인공지능 모델이 출력한 이 12개의 실수 배열 값은 실제 관절의 절대 각도가 아니라 로봇의 기본 준비 자세에서 얼마나 더 움직일지를 지시하는 목표 변화량이다. Python 코드의 Joint Position Action Cfg에서 설정한 Scale 변수가 이 12개의 Tensor 출력값에 일괄적으로 곱해져 물리적인 목표 각도로 스케일링된다.

스케일링이 완료된 12개의 목표 각도 배열은 하위 레벨의 PD 제어기로 전달되어 현재 관절 각도와의 오차를 계산한 뒤 실제 모터를 돌리는 힘으로 최종 변환된다. Tensor 출력값에 곱해지는 Scale 변수와 기본 준비 자세 각도를 조절하여 로봇의 보행 스타일을 부드럽게 튜닝할 수 있다.

실제 강화학습 개발을 위해서는 파이썬으로 코딩을 해야 한다.

1. RSL-RL 라이브러리의 PPO 알고리즘 내 Actor 신경망은 현재 로봇의 상태를 입력받아 12차원의 실수 배열(Tensor)을 출력
2. 이 12개의 값은 스팟 로봇의 네 다리에 존재하는 총 12개의 관절 모터(각 다리당 3개의 조인트)와 일대일로 매핑
3. 신경망의 순수 출력값은 환경 설정 파일에 정의된 action_scale 변수(예: 0.5)와 곱해져 물리적인 각도 변화량으로 스케일링
4. 스케일링된 값은 로봇의 기본 자세(Default Position) 각도에 더해져 각 모터가 도달해야 할 최종 목표 각도(Target Position)가 됨
5. Isaac Sim 물리 엔진 내부의 하위 레벨 PD 제어기가 이 목표 각도와 현재 관절 각도의 오차를 계산하여 실제 물리적인 모터 회전 토크(Torque)를 발생
6. AppLauncher를 통해 그래픽 렌더링을 끄고 연산 속도를 극대화하는 Headless 모드로 시뮬레이션을 초기화
7. WarehouseSceneCfg 클래스에서 USD 파일을 로드하여 창고 바닥과 스팟 로봇을 가상 환경에 배치
8. JointPositionActionCfg를 통해 신경망의 12차원 출력값이 로봇의 전체 관절(joint_names=[".*"]) 목표 위치로 매핑되도록 연결
9. RewardsCfg와 TerminationsCfg를 통해 목표 지점 전진 시 보상을 주고, 몸통이 바닥에 닿으면 에피소드를 리셋하는 규칙을 부여
10. SpotPPORunnerCfg 클래스에서 Actor와 Critic 신경망의 은닉층(Hidden Dimension)을 각각 [512, 256, 128] 노드 구조로 정의
11. RslRlVecEnvWrapper를 사용하여 IsaacLab 환경을 RSL-RL 프레임워크가 인식할 수 있는 PyTorch 텐서 환경으로 변환
12. OnPolicyRunner 객체를 생성하고 learn 메서드를 호출하여 설정된 반복 횟수만큼 GPU 기반의 본격적인 PPO 학습을 실행

이와 더불어, 카메라나 라이다 같은 센서 기반 장애물 회피 학습 메커니즘도 강화학습 시 고려해야 한다.
1. 입력 데이터의 다변화: 기존에는 관절 각도(Proprioception)만 입력받았으나, 이제는 카메라의 깊이 정보(Depth)나 라이다의 거리 데이터(Point Cloud)가 신경망의 새로운 입력값이 됨.
2. 특징 추출기(Feature Extractor) 추가: 센서 데이터는 차원이 매우 크므로, 이를 효율적으로 압축하기 위해 Actor 신경망 앞단에 CNN(이미지용)이나 PointNet(라이다용) 같은 특수 신경망 층을 배치함.
3. 상태 데이터 통합(Concatenation): 압축된 센서 특징 데이터와 현재 로봇의 관절 상태 데이터를 하나로 합쳐(Concat) 최종적인 판단을 내리는 전결합층(MLP)으로 전달함.
4. 장애물 보상 함수(Penalty) 적용: 로봇의 센서가 장애물과의 거리가 지나치게 가까워짐을 감지하거나 실제 충돌이 발생할 경우, 매우 큰 음수의 보상(Penalty)을 부여함.
5. 상관관계 자가 학습: 인공지능은 "센서 데이터에 특정 패턴(장애물)이 나타날 때 왼쪽으로 관절을 꺾으면 벌점을 피하고 계속 점수를 얻는다"는 인과관계를 수억 번의 시뮬레이션을 통해 스스로 깨우침.
6. 최종 행동 출력: 결과적으로 신경망은 센서에 찍힌 장애물 위치를 고려하여, 충돌하지 않으면서 목표 지점으로 향하는 12개 모터의 최적 각도 변화량을 계산하여 출력함.

센서 통합형 신경망 아키텍처 구조 (Conceptual)는 다음과 같다.
  • 입력층: [관절 상태(12) + 목표 속도(3) + 센서 특징(예: 128)]
  • 은닉층 (Actor): [CNN/MLP 512] -> [256] -> [128]
  • 출력층: [12개 모터 목표 각도 제어값]

실제 코드는 다음과 같다. 

import torch
import os
from omni.isaac.lab.app import AppLauncher

# 1. 시뮬레이션 초기화 (Headless 모드로 연산 효율 극대화함)
app_launcher = AppLauncher(headless=True)
simulation_app = app_launcher.app

from omni.isaac.lab.envs import ManagerBasedRLEnvCfg, ManagerBasedRLEnv
from omni.isaac.lab.managers import ObservationGroupCfg, ObservationTermCfg, RewardTermCfg, TerminationTermCfg, SceneEntityCfg
from omni.isaac.lab.scene import InteractiveSceneCfg
from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg
from omni.isaac.lab.sensors import RaycasterCfg, patterns
import omni.isaac.lab.sim as sim_utils
import omni.isaac.lab.envs.mdp as mdp
from omni.isaac.lab.utils import configclass
from omni.isaac.lab_tasks.utils.wrappers.rsl_rl import RslRlVecEnvWrapper
from rsl_rl.runners import OnPolicyRunner

# 2. 환경 설정 (Scene, Sensors, Rewards, Terminations)
@configclass
class SpotEnvCfg(ManagerBasedRLEnvCfg):
    def __init__(self):
        super().__init__()
        
        # 씬 구성: 창고 지형 및 스팟 로봇 USD 로드함
        self.scene = InteractiveSceneCfg(num_envs=4096, env_spacing=5.0)
        self.scene.terrain = AssetBaseCfg(
            prim_path="/World/ground",
            spawn=sim_utils.PlaneCfg(),
        )
        self.scene.robot = ArticulationCfg(
            prim_path="{ENV_REGEX_NS}/Spot",
            spawn=sim_utils.UsdFileCfg(
                usd_path="omniverse://localhost/NVIDIA/Assets/Isaac/5.1/Isaac/Robots/BostonDynamics/spot/spot.usd"
            ),
            init_state=ArticulationCfg.InitialStateCfg(pos=(0.0, 0.0, 0.6)),
        )

        # 센서 설정: 로봇 하단에 72개 포인트 레이캐스터(라이다) 부착함
        self.scene.lidar = RaycasterCfg(
            prim_path="{ENV_REGEX_NS}/Spot/base",
            offset=RaycasterCfg.OffsetCfg(pos=(0.0, 0.0, 0.0)),
            attach_to_parent=True,
            pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.0, 1.0)), # 주변 1m 스캔함
            mesh_prim_paths=["/World/ground"]
        )

        # 관측 데이터(Observation): 관절 상태 + 레이캐스터 데이터 통합함
        self.observations = ObservationGroupCfg(
            policy=ObservationGroupCfg.GroupCfg(
                terms={
                    "joint_pos": ObservationTermCfg(func=mdp.joint_pos_rel), # 12개 관절 각도임
                    "joint_vel": ObservationTermCfg(func=mdp.joint_vel),     # 12개 관절 속도임
                    "lidar": ObservationTermCfg(func=mdp.raycast_data, params={"sensor_cfg": "lidar"}), # 장애물 정보임
                }
            )
        )

        # 제어 출력(Action): 신경망의 12차원 출력을 모터 12개에 매핑함
        self.actions = mdp.JointPositionActionCfg(
            asset_name="robot", joint_names=[".*"], scale=0.5
        )

        # 보상 및 종료 조건 설정함
        self.rewards = {
            "track_lin_vel_xy": RewardTermCfg(func=mdp.track_lin_vel_xy_exp, weight=1.5, params={"std": 0.5}),
            "base_contact": RewardTermCfg(
                func=mdp.contact_forces_penalty, weight=-10.0, 
                params={"sensor_cfg": SceneEntityCfg("robot", body_names="body")}
            )
        }
        self.terminations = {
            "time_out": TerminationTermCfg(func=mdp.time_out),
            "base_contact": TerminationTermCfg(
                func=mdp.illegal_contact, params={"sensor_cfg": SceneEntityCfg("robot", body_names="body")}
            )
        }

# 3. PPO 학습 모델 아키텍처 정의 (Physical AI 정책망 구성함)
class SpotPPORunnerCfg:
    def __init__(self):
        self.seed = 42
        self.runner = {
            "algorithm_class_name": "PPO",
            "max_iterations": 1500,
            "experiment_name": "spot_physical_ai_training"
        }
        self.policy = {
            "class_name": "ActorCritic",
            "actor_hidden_dims": [512, 256, 128], # Actor 신경망 층임
            "critic_hidden_dims": [512, 256, 128], # Critic 신경망 층임
            "activation": "elu"
        }
        self.algorithm = {
            "class_name": "PPO",
            "value_loss_coef": 1.0,
            "clip_param": 0.2,
            "entropy_coef": 0.01,
            "learning_rate": 1e-3,
            "num_learning_epochs": 5,
            "num_mini_batches": 4,
        }

# 4. 메인 학습 루프 실행함
def main():
    # 환경 인스턴스화함
    env_cfg = SpotEnvCfg()
    env = ManagerBasedRLEnv(cfg=env_cfg)
    
    # RSL-RL 라이브러리 연동 및 훈련 설정 로드함
    wrapped_env = RslRlVecEnvWrapper(env)
    train_cfg = SpotPPORunnerCfg()
    
    # PPO Runner 실행하여 GPU 기반 학습 시작함
    runner = OnPolicyRunner(wrapped_env, train_cfg, log_dir="logs/spot_ppo_warehouse")
    runner.learn(num_learning_iterations=train_cfg.runner["max_iterations"], init_at_random_ep_len=True)
    
    wrapped_env.close()

if __name__ == "__main__":
    main()
    simulation_app.close()

이런 식으로 물리AI는 학습된다. 휴머노이드 덤블링 로봇 학습 메커니즘은 다음과 같다. 

1. 인간형 로봇의 덤블링이나 백플립 동작 역시 아이작 심과 같은 가상 환경에서 강화학습으로 완성됨.
2. 휴머노이드는 관절 수가 20~30개 이상으로 매우 복잡하여 사람이 직접 코딩하는 것이 불가능에 가까움.
3. 가상 세계에서 로봇이 공중제비를 돌다 머리로 떨어지는 실패 과정을 수억 번 반복하며 최적의 회전 토크값을 스스로 찾아냄.
4. 특히 Domain Randomization을 통해 중력이나 지면 마찰력을 극단적으로 변화시키며 어떤 상황에서도 착지할 수 있는 강인한 지능을 생성함.
5. 학습된 모델은 현장에서 Zero-shot Transfer 방식으로 투입되며 실제 로봇은 시뮬레이션에서 배운 대로 근육(모터)을 움직일 뿐임.
6. 보스턴 다이내믹스의 아틀라스나 유니트리의 인간형 로봇들이 보여주는 화려한 묘기는 대부분 이런 시뮬레이션 기반 학습의 결과물임.

현실 세계 투입 및 실시간 적응 프로세스는 다음과 같다.
1. 현장 투입 후 물리적인 시간을 들여 다시 학습(Fine-tuning)하는 것은 하드웨어 파손 위험으로 인해 지양함.
2. 대신 RMA(Rapid Motor Adaptation) 기술을 사용하여 0.1초 단위로 지면의 상태를 파악하고 신경망 출력을 보정함.
3. 로봇이 빙판길을 밟았을 때 미끄러짐을 감지하면 즉시 가상에서 배운 '미끄러운 지형 대응 모드'로 신경망이 전환됨.
4. 이는 현장에서 새로 배우는 것이 아니라 시뮬레이션에서 미리 학습해 둔 수만 가지 데이터 중 하나를 꺼내 쓰는 개념임.
5. 즉 현장 파인튜닝은 로봇이 실제 물리력을 행사하며 배우는 단계가 아니라 센서 오차를 맞추는 소프트웨어 정렬 단계에 가까움.
6. 만약 현장 데이터가 예상과 너무 다를 경우 해당 데이터를 서버로 보내 가상 환경을 업데이트한 뒤 다시 시뮬레이션 학습을 돌림.

강화학습의 핵심 마르코프체인

마르코프 체인(Markov Chain)에 '보상(Reward)'과 '행동(Action)' 개념이 추가된 마르코프 결정 과정(MDP, Markov Decision Process)이 강화학습의 수학적 토대이자 핵심 메커니즘이다.

1. 마르코프 모델을 통한 학습 데이터 생성 원리
1) 단계 데이터 생성기(Data Factory) 역할: 강화학습에서 로봇(Agent)은 가상 환경(Isaac Sim) 안에서 현재 상태(S_t)를 보고 행동(A_t)을 하며 다음 상태(S_{t+1})로 넘어가는 과정을 무수히 반복함.
2) 단계 경로(Trajectory)의 축적: 이 과정에서 `상태-행동-보상-다음 상태`(s, a, r, s')의 묶음이 하나의 데이터 포인트가 됨. 이 묶음들이 줄지어 이어진 것이 곧 마르코프 경로이며, 이것이 인공지능을 가르치는 실시간 학습 데이터가 됨.
3. 단계 보상 기반의 경로 탐색: 질문하신 대로, 각 상태 전이마다 보상(R)이 주어지므로 인공지능은 "어떤 경로(마르코프 체인)를 따라가야 누적 보상이 최대가 될 것인가?"를 계산하며 데이터를 스스로 선별하고 가공함.
4. 단계 자가 발전 학습: 처음에는 무작위로 움직이며 쓰레기 데이터를 만들지만, 점차 높은 보상을 주는 경로의 데이터를 더 많이 생성하게 되면서 모델의 성능이 기하급수적으로 올라감.

엄밀히 말하면 MDP(마르코프 결정 과정)는 강화학습의 '수학적 설계도(Framework)'이고, 이를 실제로 해결하는 PPO, Q-Learning 등이 '알고리즘(Solver)'이 된다. 강화학습 알고리즘의 핵심은 마르코프 성질을 이용하여 현재의 가치를 미래의 가치와 연결하는 벨만 방정식(Bellman Equation)을 푸는 것이다. 마르코프 모델 없이는 강화학습의 목표 설정(누적 보상 최대화) 자체가 불가능하므로, 강화학습의 심장이라고 불러도 무방하다.

2. 마르코프 체인 형식 사용 이유
1) 단계 탐색의 효율성: "기존 탐색된 해로 돌아가지 않기 위해"라기보다, "어떤 경로로 왔든 현재 위치에서 미래의 점수만 생각하면 된다"는 단순함 때문임. (과거 추적 비용 제거)
2) 단계 병렬 데이터 생성: 앞서 예시로 든 4096마리의 로봇은 각자 독립적인 마르코프 체인을 생성함. 이 방대한 양의 '가상 경로 데이터'를 한꺼번에 처리할 수 있는 구조가 마르코프 모델임.
3) 단계 물리 엔진과의 궁합: Isaac Sim의 물리 연산(F=ma) 자체가 현재 상태(S)에서 힘(A)을 주면 다음 상태(S)가 나오는 마르코프 구조이므로, 이를 그대로 인공지능 학습에 활용하는 것이 가장 효율적임.

결국, 피지컬 AI는 "가상의 마르코프 세상을 마음껏 뛰어놀며 가장 높은 점수를 얻는 움직임 데이터를 스스로 찍어내는 지능"이라고 정의할 수 있다. 이로 인한 장점은 다음과 같다.

1) 초기 단계 (Random Exploration): 학습 초기에는 지능이 없으므로 마르코프 체인이 무작위 행동을 출력함. 로봇이 제자리에서 바르르 떨거나 즉시 넘어지는 '저품질 데이터'가 생성됨.
2) 보상의 이정표 (Reward as Guide): 저품질 데이터들 사이에서도 우연히 조금 더 오래 버티거나 앞으로 한 발 나아가는 '상대적으로 좋은 데이터'가 발생함. 보상 함수(R)는 이 데이터에 높은 점수를 매김.
3) 정책 업데이트 (Policy Update): 인공지능 모델은 높은 점수를 받은 행동의 확률을 높이는 방향으로 신경망을 수정함. 이 과정에서 손실(Loss)을 줄이며 '정답'에 가까워짐.
4) 고품질 데이터 생산: 업데이트된 모델은 이제 예전보다 더 똑똑한 마르코프 체인을 생성함. 즉, 다음 차례의 학습 데이터는 이전보다 품질이 보장된 상태로 생산됨.

단계 선순환의 반복: 더 좋은 데이터 생성 -> 더 정교한 모델 학습 -> 훨씬 더 좋은 데이터 생성의 루프가 반복되며, 결국 로봇은 험지에서도 뛰어다니는 고차원 지능을 갖게 된다. 아울러, 다음과 같은 부가적 효과가 있다.

1) 과거 망각의 장점: 마르코프 성질(S_t)는 오직 S_{t-1}에만 의존함. 덕분에, 모델을 업데이트할 때 수억 번의 과거 시도 기록을 전부 메모리에 들고 있을 필요가 없음.
2) 최신 배치(Batch) 위주 학습: PPO와 같은 최신 알고리즘은 방금 생성한 따끈따끈한 마르코프 경로 데이터(Trajectory) 한 묶음만 사용해 모델을 업데이트하고, 사용이 끝난 데이터는 과감히 삭제함.
3) 메모리 절약 메커니즘: 로봇 수천 마리가 동시에 데이터를 찍어내도, 현재 학습에 필요한 최소한의 '경험치 버퍼(Experience Buffer)'만 유지하면 됨.
4) 압축된 지능: 전체 마르코프 체인의 기록 대신, 그 경험들의 정수가 녹아든 '신경망 가중치(Weights)' 하나만 저장하면 됨. 수 테라바이트의 이동 경로는 결국 몇 메가바이트의 모델 파일로 압축됨.

다음은 이를 고려한 MCP 처리와 벨만함수 적용한 강화학습 코드 예시를 보여준다. 
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

# 1. 단계: Actor 신경망 (12개 모터 제어값 출력함)
class SpotActor(nn.Module):
    def __init__(self, state_dim):
        super(SpotActor, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 256), nn.ELU(),
            nn.Linear(256, 128), nn.ELU(),
            nn.Linear(128, 12), # 출력: 12개 관절의 목표 위치임
            nn.Tanh() # -1 ~ 1 사이 범위로 제한함
        )
    def forward(self, state):
        return self.net(state)

# 2. 단계: Critic 신경망 (12개 행동의 품질(Q) 평가함)
class SpotCritic(nn.Module):
    def __init__(self, state_dim):
        super(SpotCritic, self).__init__()
        self.fc_s = nn.Linear(state_dim, 128)
        self.fc_a = nn.Linear(12, 128)
        self.fc_q = nn.Linear(256, 1) # 출력: Q-Value (품질 점수)임
    def forward(self, state, action):
        s = torch.elu(self.fc_s(state))
        a = torch.elu(self.fc_a(action))
        return self.fc_q(torch.cat([s, a], dim=1))

# 3. 단계: 보상 매니저 (스팟 로봇의 생존 및 목표 달성 판단함)
class SpotRewardManager:
    def calculate_reward(self, state, action, next_state):
        # state 구성: [몸체 속도(3), 몸체 기울기(3), 관절 상태(12), 목표 속도(3)]
        current_vel = state[:, :3]
        target_vel = state[:, 18:21]
        orientation = state[:, 3:6] # Roll, Pitch, Yaw
        
        # (1) 목표 속도 추종 보상: 타겟 방향으로 잘 가면 점수 줌
        vel_error = torch.sum((target_vel - current_vel)**2, dim=1)
        reward_forward = torch.exp(-vel_error / 0.25) * 1.0
        
        # (2) 자세 안정성 페널티: 몸체가 너무 기울어지면 감점함
        penalty_tilt = torch.sum(orientation**2, dim=1) * -0.5
        
        # (3) 에너지 효율 페널티: 12개 관절을 너무 급격히 움직이면 감점함
        penalty_action = torch.sum(action**2, dim=1) * -0.01
        
        # (4) 생존 보상: 넘어지지 않고 살아있으면 기본 점수 줌
        reward_alive = 0.1
        
        total_reward = reward_forward + penalty_tilt + penalty_action + reward_alive
        return total_reward.unsqueeze(1)

# 4. 단계: 벨만 방정식 기반 학습 엔진
def train_spot(actor, critic, target_critic, batch, reward_manager, gamma=0.99):
    states, actions, next_states, dones = batch
    
    # [Reward] 물리 기반 보상 함수로 점수 계산함
    rewards = reward_manager.calculate_reward(states, actions, next_states)
    
    # [Current Q] 현재 예측한 12개 행동의 품질임
    current_q = critic(states, actions)
    
    # [Bellman Target] 미래의 기대 가치를 현재로 소환함
    with torch.no_grad():
        next_actions = actor(next_states)
        # 타겟 Q = r + gamma * Q(s', a')
        target_q = rewards + (gamma * target_critic(next_states, next_actions) * (1 - dones))
    
    # [Loss] 벨만 오차(TD Error)를 줄여 지능을 높임
    loss = torch.nn.functional.mse_loss(current_q, target_q)
    return loss

# 5. 단계: 메인 학습 루프 (마르코프 결정 과정)
# (실제 환경 실행 시 actor에서 나온 12개 값을 스팟 모터에 주입함)

피지컬AI와 로봇제어

이와 같이, 강화학습을 통해 가상 세계의 지능을 물리적 실체에 이식하는 기술을 피지컬AI(Physical AI)라고 부른다. 

Physical AI는 인공지능이 디지털 환경에만 머물지 않고 실제 물리적 세계와 상호작용하며 동작하는 기술을 의미한다. 텍스트나 이미지만 생성하는 생성형 AI와 달리 중력과 마찰력 등 물리 법칙이 지배하는 환경에서 신체를 제어하는 것이 핵심이다. Isaac Sim과 같은 시뮬레이터에서 Reinforcement Learning으로 학습된 지능은 Physical AI의 뇌 역할을 수행한다. NVIDIA와 같은 빅테크 기업들은 이를 차세대 AI의 핵심 분야로 정의하고 있으며 로보틱스가 그 중심에 있다. Physical AI는 시각 데이터인 Computer Vision과 신체 제어인 Actuation이 결합된 형태를 띠고 있다.

Physical AI를 구현하는 주요 기술들은 다음과 같다.
1. Imitation Learning (IL): 사람이 로봇을 직접 조종하며 보여준 동작 데이터를 보고 그대로 따라 배우게 하는 방식임. 강화학습처럼 수억 번 넘어질 필요 없이, 전문가의 데이터를 통해 효율적으로 동작을 익힐 수 있어 최근 주방 보조 로봇 등에 많이 쓰임.

2. Model Predictive Control (MPC): 물리 법칙(중력, 관성 등)을 수학적으로 계산하여 실시간으로 최적의 힘을 계산하는 전통적인 제어 방식임. 강화학습처럼 데이터에 의존하지 않고 정교한 물리 계산을 바탕으로 하므로, 안정성이 매우 높고 안전이 중요한 산업용 로봇에 필수적임.

3. Foundation Models (Vision-Language-Action): 최근 챗GPT와 같은 거대 모델을 로봇에 이식한 형태임. "냉장고에서 콜라 가져와"라는 언어 명령을 이해하고, 시각 데이터와 신체 제어 명령(Action)을 한 번에 연결하는 차세대 기술임.

4. Hybrid AI (RL + MPC): 전통적인 물리 제어(MPC)의 안정성과 강화학습(RL)의 유연함을 결합한 형태임. 기본 중심 잡기는 MPC가 담당하고, 험지 보행이나 덤블링 같은 복잡한 판단은 RL이 담당하는 방식이 실제 상용 로봇에서 가장 많이 활용됨.

피지컬AI 적용 사례

보스턴 다이내믹스(Boston Dynamics)와 유니트리(Unitree)가 로봇의 움직임을 구현하기 위해 사용하는 최신 학습 메커니즘과 방법론은 다음과 같다.ㅏ

1. 유니트리(Unitree)의 학습 메커니즘: 순수 RL 기반 Sim-to-Real
유니트리는 가성비와 빠른 양산을 위해 데이터 기반의 강화학습(RL)에 매우 공격적으로 투자하고 있음.
1) Massive Parallel Simulation: 엔비디아의 Isaac Gym이나 Isaac Lab을 사용하여 가상 환경에서 수천 마리의 로봇을 동시에 학습시킴.
2) RMA (Rapid Motor Adaptation): 로봇이 걷는 동안 발바닥의 마찰력이나 지면의 단단함을 실시간으로 추정하여 신경망의 출력을 보정하는 기술을 적용함.
3) End-to-End 제어: 센서 데이터를 입력하면 중간 단계 없이 바로 12개 모터의 토크(Torque)나 관절 각도를 출력하는 방식을 선호함.
4) Zero-shot Transfer: 시뮬레이션에서 험지, 얼음판, 계단 등을 미리 학습시킨 후, 실제 로봇에서는 추가 학습 없이 바로 구동하는 방식을 사용함.

2. 보스턴 다이내믹스(Boston Dynamics)의 학습 메커니즘: Hybrid Control
전통적인 로봇 공학의 강자인 보스턴 다이내믹스는 수학적 모델링(MPC)과 강화학습(RL)을 결합한 하이브리드 방식을 사용한다.
1) MPC (Model Predictive Control) 기반의 중심 잡기: 로봇의 균형을 잡거나 기본적인 보행은 물리 법칙을 계산하는 수학적 모델링으로 처리함. 이는 매우 안정적임.
2) RL 기반의 고난도 동작: 덤블링, 춤추기, 파쿠르와 같이 수학적으로 계산하기 너무 복잡한 동작은 강화학습을 통해 지능을 부여함.
3) Whole-Body Control (WBC): 머리부터 발끝까지 모든 관절을 하나의 통합된 시스템으로 제어하여, 한 부분이 흔들려도 다른 부분이 보상하여 균형을 유지하게 함.
4) Vision-Language-Action (VLA): 최근 공개된 전동식 아틀라스(Electric Atlas)는 챗GPT와 같은 거대 모델을 결합하여 사람의 언어 명령을 이해하고 행동하는 단계까지 진화함.

부록: 물리AI 학습방법

참고로, 물리AI(Physics AI)는 근본적으로 물리적 재료나 구조를 해석, 예측하기 위한 목적으로 물리적 수식을 손실함수에 적용해 학습 개발하는 차이가 있다. 피지컬 AI(Physical AI)가 로봇의 '움직임'에 집중한다면, 피직스 AI(Physics AI)는 물리 법칙 그 자체를 신경망에 내장하여 시뮬레이션이나 구조 해석을 수행하는 기술로 서로 차이가 있다. 

예를 들어, 교량이나 기둥의 처짐을 예측할 때 사용하는 가장 대표적인 모델 구조는 PINN(Physics-Informed Neural Networks)이다.

피직스 AI 구조물 해석 모델의 메커니즘은 다음과 같다. 
1. 수학적 모델 정의: 오일러-베르누이 보 방정식과 같은 물리 공식을 신경망의 학습 규칙으로 설정함.
2. 입력 데이터 구성: 구조물의 위치(x), 하중(q), 재료 물성(영률 E, 단면 이차 모멘트 I), 가해지는 하중(q)을 입력 텐서로 정의함.
3. 단계 신경망 아키텍처: 입력값 x를 받아 해당 지점의 처짐량(w)을 출력하는 다층 퍼셉트론(MLP) 구조를 사용.
4. 물리 손실 함수(Physics Loss): 자동 미분(Auto-differentiation)을 통해 출력값의 4차 미분값을 구하고, 이것이 물리 공식과 일치하는지 체크하여 오차를 줄임.
5. 단계 경계 조건(Boundary Conditions): 기둥의 양 끝이 고정되었는지, 혹은 힌지로 연결되었는지에 대한 구속 조건을 손실 함수에 포함하여 실제 물리 상황을 모사함.

다음은 관련 학습 코드를 보여준다.
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

# 1. 단계: 하중을 입력으로 받는 범용 PINN 모델 (beam의 처짐 예측)
class PINN_MultiLoad(nn.Module):
    def __init__(self):
        super(PINN_MultiLoad, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64), nn.Tanh(),   # 입력: (x, q) 2차원. 위치와 하중값
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 1)
        )

    def forward(self, xq):  # xq: (batch, 2) - [x, q]
        return self.net(xq)

# 2. 단계: 물리 법칙 및 경계 조건 포함 손실 함수. Euler-Bernoulli Beam Theory 고려.
def compute_loss(model, xq_train, w_train, xq_physics, E, I, L, w_normalize, device):
    # (1) Data Loss: 계측 데이터와의 오차
    w_pred = model(xq_train)
    loss_data = torch.mean((w_pred - w_train)**2) # 처짐 예측과 실제 처짐 간의 MSE

    # (2) Physics Loss: PDE 검증
    xq_physics_grad = xq_physics.clone().requires_grad_(True)
    w = model(xq_physics_grad)
    
    # 자동 미분 (x에 대해서만). Euler-Bernoulli 보 이론에 따르면, d⁴w/dx⁴ = q/(E*I) 이므로, 4차 미분이 필요함. 그래야, 모델이 다양한 하중 조건에서 물리 법칙인 d⁴w/dx⁴ = q/(E*I)를 만족하도록 학습할 수 있음.
    dw_dx = torch.autograd.grad(w, xq_physics_grad, torch.ones_like(w), create_graph=True)[0][:, 0:1] # x에 대한 1차 미분. 처짐의 변화율. 기울기
    d2w_dx2 = torch.autograd.grad(dw_dx, xq_physics_grad, torch.ones_like(dw_dx), create_graph=True)[0][:, 0:1] # x에 대한 2차 미분. 곡률. 휨
    d3w_dx3 = torch.autograd.grad(d2w_dx2, xq_physics_grad, torch.ones_like(d2w_dx2), create_graph=True)[0][:, 0:1] # x에 대한 3차 미분. 곡률의 변화율. 휨의 변화
    d4w_dx4 = torch.autograd.grad(d3w_dx3, xq_physics_grad, torch.ones_like(d3w_dx3), create_graph=True)[0][:, 0:1] # x에 대한 4차 미분. 휨의 변화율. 보의 강성에 대한 저항력
    
    # q는 입력에서 추출 (역정규화)
    q_batch = xq_physics[:, 1:2] * 20000.0 - 10000.0  # 정규화. q를 실제 하중 범위로 변환. q는 음수이므로 -10000에서 시작하여 20000 범위로 확장됨. 예: q=0 → -10000 N/m, q=1 → 10000 N/m. 만약, q가 -15000에서 -5000 범위라면, q_batch = xq_physics[:, 1:2] * 10000.0 - 15000.0로 조정해야 함.
    
    # 정규화된 PDE: d⁴w̄/dx̄⁴ = (q*L⁴)/(E*I*w_max)
    q_normalized = (q_batch * L**4) / (E * I * w_normalize) # q를 정규화하여 PDE의 잔차 계산. 참고로 PDE 약어는 Partial Differential Equation의 약자로 부분 미분 방정식을 의미함. 보의 처짐과 하중 사이의 관계를 나타내는 PDE의 잔차를 계산하여 모델이 물리 법칙을 얼마나 잘 만족하는지 평가함.
    pde_residual = d4w_dx4 - q_normalized # 모델이 예측한 분포하중 q에 대한 4차 미분과 실제 q에 대한 정규화된 PDE의 차이
    loss_physics = torch.mean(pde_residual**2)

    # (3) Boundary Condition Loss: 다양한 q에 대해 경계 조건
    # 샘플링: 여러 q 값에서 경계 조건 테스트
    num_bc_samples = 10
    q_bc = torch.linspace(0, 1, num_bc_samples).view(-1, 1).to(device)
    
    # x=0에서 w=0. 보의 양끝은 고정되어 있다고 가정
    xq_bc_0 = torch.cat([torch.zeros(num_bc_samples, 1, device=device), q_bc], dim=1)
    w_bc_0 = model(xq_bc_0)
    
    # x=1에서 w=0. 보의 양끝은 고정되어 있다고 가정
    xq_bc_L = torch.cat([torch.ones(num_bc_samples, 1, device=device), q_bc], dim=1)
    w_bc_L = model(xq_bc_L)
    
    loss_bc = torch.mean(w_bc_0**2) + torch.mean(w_bc_L**2) # 양 끝단에서 처짐이 0이 되도록 강제

    # 전체 손실
    weight_data = 1.0       # 데이터 손실의 중요성
    weight_physics = 3.0    # 물리PDE의 중요성
    weight_bc = 50.0        # 경계 조건의 중요성
    
    return weight_data * loss_data + weight_physics * loss_physics + weight_bc * loss_bc

# 3. 단계: 다양한 하중 조건에 대한 데이터 생성
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"사용 장치: {device}")

L, E, I = 10.0, 210e9, 0.0001

# 여러 하중 조건 생성 (q: -15000 ~ -5000 N/m 범위)
q_values = np.linspace(-15000, -5000, 10)  # 10가지 하중 조건
x_positions = np.linspace(0, L, 21)  # 각 하중마다 21개 위치

# 전체 데이터 생성
xq_train_list = []
w_train_list = []

for q in q_values:
    for x in x_positions[::2]:  # 10개 포인트만 사용
        x_norm = x / L
        q_norm = (q + 10000) / 20000  # q를 [0,1] 범위로 정규화
        
        # 해석해
        w_true = (q * x / (24 * E * I)) * (L**3 - 2 * L * x**2 + x**3)
        
        xq_train_list.append([x_norm, q_norm])
        w_train_list.append([w_true])

xq_train_np = np.array(xq_train_list)
w_train_np = np.array(w_train_list)

# 정규화
w_normalize = np.abs(w_train_np).max()
w_train_norm = w_train_np / w_normalize

print(f"데이터 정규화: x_scale={L}, w_scale={w_normalize:.6e}, q_range=[-15000, -5000]")
print(f"훈련 데이터 수: {len(xq_train_list)}")

# 텐서 변환
xq_train = torch.FloatTensor(xq_train_np).to(device)
w_train = torch.FloatTensor(w_train_norm).to(device)

# 물리 법칙 검증용 포인트 (여러 q 값)
x_phys = torch.linspace(0, 1, 50).view(-1, 1)
q_phys = torch.rand(50, 1)  # 랜덤 q 값
xq_physics = torch.cat([x_phys, q_phys], dim=1).to(device)

# 4. 단계: 모델 및 학습 설정
model = PINN_MultiLoad().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=5e-4)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=500)

# 5. 단계: 학습 루프
print("범용 하중 조건 PINN 학습 시작...")
epochs = 20000

for epoch in range(epochs + 1):
    optimizer.zero_grad()
    loss = compute_loss(model, xq_train, w_train, xq_physics, E, I, L, w_normalize, device)
    loss.backward()
    optimizer.step()
    scheduler.step(loss)
    
    if epoch % 1000 == 0:
        current_lr = optimizer.param_groups[0]['lr']
        print(f"Epoch {epoch}: Total Loss {loss.item():.6f}, LR={current_lr:.6e}")

# 6. 단계: 테스트 - 여러 하중 조건에서 예측
model.eval()
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
test_loads = [-15000, -10000, -7500, -5000]

for idx, (ax, q_test) in enumerate(zip(axes.flat, test_loads)):
    with torch.no_grad():
        # 테스트 입력 생성
        x_test = torch.linspace(0, 1, 100).view(-1, 1)
        q_test_norm = (q_test + 10000) / 20000
        q_test_tensor = torch.full((100, 1), q_test_norm)
        xq_test = torch.cat([x_test, q_test_tensor], dim=1).to(device)
        
        # 예측
        w_pred_norm = model(xq_test).cpu().numpy()
        w_pred = w_pred_norm * w_normalize
        
        # 해석해
        x_full = np.linspace(0, L, 100)
        w_exact = (q_test * x_full / (24 * E * I)) * (L**3 - 2 * L * x_full**2 + x_full**3)
    
    ax.plot(x_full, w_exact, 'r-', linewidth=2, label='Exact Solution')
    ax.plot(x_full, w_pred, 'b--', linewidth=2, label='PINN Prediction')
    ax.set_xlabel('Position x (m)')
    ax.set_ylabel('Deflection w (m)')
    ax.set_title(f'Load q = {q_test} N/m')
    ax.legend()
    ax.grid(True)

plt.tight_layout()
plt.savefig('pinn_multiload_results.png', dpi=150)
plt.show()

# 7. 모델 사용 예시
print("\n모델 사용 예시")
def predict_deflection(model, x_meter, q_load, L, w_normalize, device):
    """임의의 위치와 하중에서 처짐 예측"""
    x_norm = x_meter / L
    q_norm = (q_load + 10000) / 20000
    xq_input = torch.tensor([[x_norm, q_norm]], device=device)
    
    with torch.no_grad():
        w_norm = model(xq_input).cpu().item()
        w_real = w_norm * w_normalize
    return w_real

# 테스트
test_cases = [
    (5.0, -10000),  # 중앙, 원래 하중
    (5.0, -12000),  # 중앙, 더 큰 하중
    (3.0, -8000),   # 왼쪽, 작은 하중
]

for x, q in test_cases:
    w_pred = predict_deflection(model, x, q, L, w_normalize, device)
    print(f"위치 {x}m, 하중 {q} N/m → 예측 처짐: {w_pred:.6f} m")

PINN 모델 학습 결과는 다음과 같다. 

Epoch 0: Total Loss 2856.965088, LR=5.000000e-04
Epoch 1000: Total Loss 0.239555, LR=5.000000e-04
Epoch 2000: Total Loss 0.065799, LR=5.000000e-04
Epoch 3000: Total Loss 0.035432, LR=5.000000e-04
Epoch 4000: Total Loss 0.030447, LR=5.000000e-04
Epoch 5000: Total Loss 0.080436, LR=5.000000e-04
Epoch 6000: Total Loss 0.020961, LR=5.000000e-04
Epoch 7000: Total Loss 0.250146, LR=5.000000e-04
Epoch 8000: Total Loss 0.017407, LR=5.000000e-04
Epoch 9000: Total Loss 0.016900, LR=5.000000e-04
Epoch 10000: Total Loss 0.016144, LR=5.000000e-04
Epoch 11000: Total Loss 0.015525, LR=5.000000e-04
Epoch 12000: Total Loss 0.014678, LR=5.000000e-04
Epoch 13000: Total Loss 0.013370, LR=5.000000e-04
Epoch 14000: Total Loss 0.786475, LR=5.000000e-04
Epoch 15000: Total Loss 0.015058, LR=5.000000e-04
Epoch 16000: Total Loss 0.010801, LR=2.500000e-04
Epoch 17000: Total Loss 0.008572, LR=2.500000e-04
Epoch 18000: Total Loss 0.045776, LR=2.500000e-04
Epoch 19000: Total Loss 0.006842, LR=2.500000e-04
Epoch 20000: Total Loss 0.205008, LR=2.500000e-04

모델 사용 예시
위치 5.0m, 하중 -10000 N/m → 예측 처짐: -0.070491 m
위치 5.0m, 하중 -12000 N/m → 예측 처짐: -0.081096 m
위치 3.0m, 하중 -8000 N/m → 예측 처짐: -0.044906 m
물리AI기반 각 지점, 하중별 쳐짐량 계산

이런 이유로 PINN 일반화 공식은 다음과 같이 정의될 수 있다.
loss = loss_data + λ_physics * loss_physics + λ_bc * loss_bc

where:
  loss_physics = ||PDE(NN(x)) - RHS||²
  PDE(NN(x)) = 신경망 출력의 미분 조합
  RHS = 물리 방정식의 우변

그러므로, 학습 모델도 다음과 같이 일반화될 수 있다. 
class GeneralPINN(nn.Module):
    def __init__(self, input_dim, output_dim, hidden_size=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_size), nn.Tanh(),
            nn.Linear(hidden_size, hidden_size), nn.Tanh(),
            nn.Linear(hidden_size, hidden_size), nn.Tanh(),
            nn.Linear(hidden_size, output_dim)
        )
    
    def forward(self, x):
        return self.net(x)

def general_pde_loss(model, x_physics, pde_func, device):
    """
    pde_func: 사용자 정의 PDE 계산 함수
              입력: (model, x_physics_grad)
              출력: pde_residual
    """
    x = x_physics.clone().requires_grad_(True)
    
    # 사용자 정의 PDE 계산
    pde_residual = pde_func(model, x)
    
    loss_physics = torch.mean(pde_residual**2)
    return loss_physics

# 사용 예
def my_custom_pde(model, x):
    # 여기에 당신의 PDE 정의
    u = model(x)
    # ... 미분 계산 ...
    # ... PDE residual 반환 ...
    return residual

loss = general_pde_loss(model, x_physics, my_custom_pde, device)

다음은 PINN이 적용될 수 있는 모델 예시이다. 

분야       PDE                             PINN 입력      PINN 출력
구조역학 Euler-Bernoulli                 (x, q)             처짐 w
열전달       Heat Equation                 (x, t)             온도 T
유체역학 Navier-Stokes                 (x, y, t)      (u, v, p)
전자기학 Maxwell                       (x, y, z, t)      (E, B)
양자역학 Schrödinger                 (x, t)             ψ
지진학       Wave Equation                 (x, y, z, t)      변위
재무      Black-Scholes                 (S, t)          옵션 가격
생물      Reaction-Diffusion           (x, t)          농도

이런 방식으로 열전도는 다음과 같이 구현될 수 있다.
# 1D 열전도 PINN
class HeatPINN(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64), nn.Tanh(),  # 입력: (x, t)
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 1)  # 출력: T(x,t) 온도
        )
    
    def forward(self, xt):
        return self.net(xt)

def heat_loss(model, xt_physics, alpha, device):
    xt = xt_physics.clone().requires_grad_(True)
    T = model(xt)
    
    # 자동 미분
    T_grad = torch.autograd.grad(T, xt, torch.ones_like(T), create_graph=True)[0]
    dT_dt = T_grad[:, 1:2]  # ∂T/∂t
    dT_dx = T_grad[:, 0:1]  # ∂T/∂x
    
    d2T_dx2 = torch.autograd.grad(dT_dx, xt, torch.ones_like(dT_dx), create_graph=True)[0][:, 0:1]
    
    # PDE: ∂T/∂t - α·∂²T/∂x² = 0
    pde_residual = dT_dt - alpha * d2T_dx2
    loss_physics = torch.mean(pde_residual**2)
    
    return loss_physics

# 사용 예
# 입력: (x, t) → 출력: 온도 T(x, t)
# "10초 후 5cm 위치의 온도는?"

만약, 복잡한 트러스트 구조나 3차원 셀 구조는 PINN을 단일 부재로 가정해 학습 사용하거나, 경계조건에서는 BC 손실이 0으로 강제 처리되도록 기하학적 임베딩 마스크를 사용하거나, 전체 해석은 FEM이 맡고 세부는 PINN이 해석하는 혼합된 하이브리드 방법을 사용할 수 있다.

문제 분해 접근을 통한 단일 부재 예시 구조
class TrussPINN(nn.Module):
    def __init__(self):
        # 입력: [member_id, x_position, axial_force]
        self.net = nn.Sequential(
            nn.Linear(3, 128), nn.Tanh(),
            # ... layers
            nn.Linear(128, 2)  # [displacement, stress]
        )

레퍼런스