웹사이트 검색

비전-언어-액션 모델 미세 조정


소개

비전-언어-행동(VLA) 모델은 시각적 인식, 언어 이해, 로봇 제어를 통합하여 자연어 명령을 따라 물리적 작업을 수행할 수 있는 통합 모델로, 구현형 AI 분야에서 획기적인 발전을 나타냅니다. 이 튜토리얼에서는 특정 로봇 응용 프로그램에 맞게 VLA 모델을 미세 조정하는 전체 과정을 다룹니다.

비전-언어-액션 모델이란 무엇인가요?

VLA는 컴퓨터 비전(이미지/비디오 해석), 자연어 처리(텍스트 이해 및 생성), 행동 실행(환경이나 시스템과 상호작용)을 결합합니다. 이를 통해 시각적 및 텍스트 입력을 기반으로 인지하고, 추론하며, 행동할 수 있습니다.

VLA가 실제로 어떻게 작동하는지 보고 싶다면, Hannah Fry와 함께하는 구글 딥마인드 로보틱스 연구소 투어를 확인해 보세요.

인기 있는 VLA 아키텍처에는 OpenVLA, RT-2(로보틱 트랜스포머 2), PaLM-E가 있습니다.

핵심 내용

  • VLA 모델은 엔드 투 엔드 아키텍처에서 카메라 이미지와 자연어 지시를 로봇 동작으로 직접 매핑함으로써 시각, 언어, 행동을 통합합니다.
  • LoRA를 사용하면 전체 모델 대신 0.1-0.2%의 파라미터만 학습하여 효율적인 미세 조정이 가능하며, 24GB VRAM을 가진 단일 GPU에서도 학습할 수 있습니다.

전제 조건

하드웨어 요구 사항

  • 최소 24GB VRAM을 가진 GPU (RTX 3090/4090, A100, 또는 H100)
  • 로봇 하드웨어 또는 시뮬레이션 환경
  • 추론 실행 시 시각 입력을 위한 카메라
  • 로봇 하드웨어 또는 시뮬레이션 환경

단계 0: GPU와 Jupyter Notebook 설정

Linux-Console.net GPU Droplet를 설정한 후 터미널에서 SSHroot@[IPV4]로 접속하세요

터미널에서:

# Core dependencies
pip install torch torchvision
pip install transformers accelerate
pip install datasets
pip install wandb  # for experiment tracking
pip install jupyter lab
jupyter lab

1단계: VLA 아키텍처 이해하기

주요 구성 요소: 비전 인코더: 카메라 이미지를 시각적 임베딩으로 처리 언어 인코더: 텍스트 명령을 언어 임베딩으로 변환 융합 모듈: 시각 정보와 언어 정보를 결합 액션 디코더: 융합 표현에서 로봇 동작 예측

단계 2: 데이터셋 준비하기

데이터 수집

데이터셋에는 다음과 같은 에피소드가 포함되어야 합니다:

  • 이미지: 각 시간 단계에서의 RGB 카메라 관측
  • 언어: 자연어 작업 설명
  • 동작: 로봇 동작 시퀀스(관절 위치, 속도 또는 엔드 이펙터 자세)
  • 메타데이터: 성공 라벨, 에피소드 길이 등 등.

데이터 형식 예시

{
    "episode_0": {
        "images": [img_0, img_1, ..., img_T],  # shape: (T, H, W, 3) where T is total frames
        "language": "pick up the red block",
        "actions": [act_0, act_1, ..., act_T],  # shape: (T, action_dim)
        "success": True
    }
}

데이터셋 구성

from datasets import Dataset, DatasetDict
import numpy as np

def create_vla_dataset(episodes):
    """Convert robot episodes to HuggingFace dataset format"""
    
    data = {
        "images": [],
        "language": [],
        "actions": [],
        "episode_id": []
    }
    
    for ep_id, episode in enumerate(episodes):
        for t in range(len(episode['images'])):
            data['images'].append(episode['images'][t])
            data['language'].append(episode['language'])
            data['actions'].append(episode['actions'][t])
            data['episode_id'].append(ep_id)
    
    return Dataset.from_dict(data)

# Split into train/val
dataset = create_vla_dataset(your_episodes)
dataset = dataset.train_test_split(test_size=0.1)

3단계: 기본 모델 설정

사전 학습된 VLA 로딩

from transformers import AutoModel, AutoProcessor
import torch

# Load pre-trained VLA (example using OpenVLA)
model_name = "openvla/openvla-7b"
processor = AutoProcessor.from_pretrained(model_name)
model = AutoModel.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto"
)

# Freeze base model parameters (optional for LoRA)
for param in model.parameters():
    param.requires_grad = False

행동 공간 적응

당신 로봇의 행동 공간은 아마도 사전 학습 데이터와 다를 것입니다:

from torch import nn

class ActionHead(nn.Module):
    def __init__(self, hidden_dim, action_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(hidden_dim, 512),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(512, action_dim)
        )
    
    def forward(self, x):
        return self.fc(x)

# Add custom action head
model.action_head = ActionHead(
    hidden_dim=model.config.hidden_size,
    action_dim=7  # e.g., 6-DOF arm + gripper
)

4단계: LoRA를 활용한 효율적인 파인튜닝

저랭크 적응(LoRA)은 학습 가능한 파라미터 수를 줄여 효율적인 미세 조정을 가능하게 합니다:

from peft import LoraConfig, get_peft_model

# Configure LoRA
lora_config = LoraConfig(
    r=16,  # rank
    lora_alpha=32,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
    lora_dropout=0.1,
    bias="none",
    task_type="CAUSAL_LM"
)

# Apply LoRA to model
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
#Expected output: trainable params: 8.3M || all params: 7B || trainable%: 0.12%

5단계: 데이터 처리 및 증강

이미지 전처리

from torchvision import transforms

image_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                        std=[0.229, 0.224, 0.225])
])

행동 정규화

def normalize_actions(actions, stats):
    """Normalize actions to [-1, 1] range"""
    return (actions - stats['mean']) / (stats['std'] + 1e-8)

def denormalize_actions(normalized_actions, stats):
    """Convert back to original action space"""
    return normalized_actions * (stats['std'] + 1e-8) + stats['mean']

# Calculate statistics from your dataset
action_stats = {
    'mean': dataset['train']['actions'].mean(axis=0),
    'std': dataset['train']['actions'].std(axis=0)
}

데이터로더 설정

from torch.utils.data import DataLoader

def collate_fn(batch):
    """Custom collate function for VLA data"""
    images = torch.stack([processor.image_processor(b['images']) 
                          for b in batch])
    text = processor.tokenizer([b['language'] for b in batch], 
                               padding=True, return_tensors="pt")
    actions = torch.tensor([b['actions'] for b in batch], 
                           dtype=torch.float32)
    
    return {
        'pixel_values': images,
        'input_ids': text['input_ids'],
        'attention_mask': text['attention_mask'],
        'actions': actions
    }

train_loader = DataLoader(
    dataset['train'],
    batch_size=32,
    shuffle=True,
    collate_fn=collate_fn,
    num_workers=4
)

6단계: 학습 루프

손실 함수

import torch.nn.functional as F

def vla_loss(predicted_actions, target_actions, reduction='mean'):
    """Action prediction loss (MSE for continuous actions)"""
    mse_loss = F.mse_loss(predicted_actions, target_actions, reduction=reduction)
    return mse_loss

# Alternative: Action chunking for temporal coherence
def chunked_action_loss(pred_chunks, target_chunks, chunk_size=10):
    """Predict multiple future actions at once"""
    loss = 0
    for i in range(chunk_size):
        loss += F.mse_loss(pred_chunks[:, i], target_chunks[:, i])
    return loss / chunk_size

훈련 스크립트

from accelerate import Accelerator
from tqdm import tqdm
import wandb

# Initialize accelerator for distributed training
accelerator = Accelerator(mixed_precision='bf16')

# Setup
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10000)

# Prepare for distributed training
model, optimizer, train_loader = accelerator.prepare(
    model, optimizer, train_loader
)

# Initialize wandb
wandb.init(project="vla-finetuning", config={
    "learning_rate": 1e-4,
    "batch_size": 32,
    "epochs": 10
})

# Training loop
global_step = 0
for epoch in range(10):
    model.train()
    epoch_loss = 0
    
    for batch in tqdm(train_loader, desc=f"Epoch {epoch+1}"):
        # Forward pass
        outputs = model(
            pixel_values=batch['pixel_values'],
            input_ids=batch['input_ids'],
            attention_mask=batch['attention_mask']
        )
        
        # Predict actions
        predicted_actions = model.action_head(outputs.last_hidden_state[:, -1])
        
        # Compute loss
        loss = vla_loss(predicted_actions, batch['actions'])
        
        # Backward pass
        accelerator.backward(loss)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()
        
        # Logging
        epoch_loss += loss.item()
        global_step += 1
        
        if global_step % 100 == 0:
            wandb.log({
                "loss": loss.item(),
                "learning_rate": scheduler.get_last_lr()[0],
                "epoch": epoch
            })
    
    print(f"Epoch {epoch+1} - Average Loss: {epoch_loss / len(train_loader):.4f}")
    
    # Save checkpoint
    if (epoch + 1) % 2 == 0:
        accelerator.save_model(model, f"checkpoints/epoch_{epoch+1}")

단계 7: 평가

시뮬레이션 평가

def evaluate_in_simulation(model, env, num_episodes=50):
    """Evaluate model in simulation environment"""
    model.eval()
    success_count = 0
    
    with torch.no_grad():
        for episode in range(num_episodes):
            obs = env.reset()
            instruction = env.get_task_instruction()
            done = False
            
            while not done:
                # Process observation
                image = torch.tensor(obs['image']).unsqueeze(0)
                text_inputs = processor.tokenizer(instruction, return_tensors="pt")
                
                # Predict action
                outputs = model(
                    pixel_values=image.to(model.device),
                    input_ids=text_inputs['input_ids'].to(model.device)
                )
                action = model.action_head(outputs.last_hidden_state[:, -1])
                
                # Denormalize and execute
                action = denormalize_actions(action.cpu().numpy(), action_stats)
                obs, reward, done, info = env.step(action[0])
            
            if info['success']:
                success_count += 1
    
    success_rate = success_count / num_episodes
    print(f"Success Rate: {success_rate*100:.1f}%")
    return success_rate

행동 복제 지표

def evaluate_bc_metrics(model, val_loader):
    """Evaluate behavioural cloning performance"""
    model.eval()
    total_mse = 0
    total_cosine_sim = 0
    n_samples = 0
    
    with torch.no_grad():
        for batch in val_loader:
            outputs = model(
                pixel_values=batch['pixel_values'],
                input_ids=batch['input_ids']
            )
            pred_actions = model.action_head(outputs.last_hidden_state[:, -1])
            
            # MSE
            mse = F.mse_loss(pred_actions, batch['actions'])
            total_mse += mse.item() * len(batch['actions'])
            
            # Cosine similarity
            cosine_sim = F.cosine_similarity(pred_actions, batch['actions']).mean()
            total_cosine_sim += cosine_sim.item() * len(batch['actions'])
            
            n_samples += len(batch['actions'])
    
    return {
        'mse': total_mse / n_samples,
        'cosine_similarity': total_cosine_sim / n_samples
    }

단계 8: 배포

모델 내보내기

# Save fine-tuned model
model.save_pretrained("./finetuned_vla")
processor.save_pretrained("./finetuned_vla")

# For deployment, merge LoRA weights
from peft import PeftModel

base_model = AutoModel.from_pretrained(model_name)
merged_model = PeftModel.from_pretrained(base_model, "./finetuned_vla")
merged_model = merged_model.merge_and_unload()
merged_model.save_pretrained("./deployed_model")

실시간 추론

class VLAController:
    def __init__(self, model_path):
        self.model = AutoModel.from_pretrained(model_path)
        self.processor = AutoProcessor.from_pretrained(model_path)
        self.model.eval()
        self.model.to('cuda')
    
    @torch.inference_mode()
    def predict_action(self, image, instruction):
        """Real-time action prediction"""
        # Preprocess
        inputs = self.processor(
            images=image,
            text=instruction,
            return_tensors="pt"
        ).to('cuda')
        
        # Predict
        outputs = self.model(**inputs)
        action = self.model.action_head(outputs.last_hidden_state[:, -1])
        
        return action.cpu().numpy()[0]

# Usage
controller = VLAController("./deployed_model")
action = controller.predict_action(camera_image, "pick up the cup")
robot.execute_action(action)

자원

  • 오픈VLA

  • RT-2 논문과 RT-2 웹사이트: 이것이 VLA(비전-언어-행동 모델)라는 용어를 만든 작업입니다

  • PEFT 라이브러리

  • RoboMimic (데이터셋 처리용)

  • OpenVLA: 김무진의 LeRobot 연구 발표 #5

결론

VLA 모델을 미세 조정하면 로봇이 자연어로 제어되는 특화된 작업을 수행할 수 있습니다. 이 튜토리얼이 사전 학습된 모델을 특정 하드웨어와 작업에 맞게 조정하는 방법을 보여주고, 대규모 사전 학습의 장점을 활용하면서도 응용에 맞게 커스터마이징할 수 있기를 바랍니다. 여기서는 시뮬레이션부터 시작하고, 빠르게 반복하며, 모델이 개선됨에 따라 점차 실제 하드웨어로 전환하는 것이 좋습니다.

Linux-Console.net 커뮤니티와 함께 학습해 주셔서 감사합니다.