728x90
728x90
이 코드를 천천히 따라가면 이런 loss 그래프를 그릴 수 있다.
한글이 깨진건지 글자는 많이 깨져 있다...

📌 전체 코드 설명
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(model_name)
model.cuda()
model.train()
1. AutoTokenizer.from_pretrained(model_name, use_fast=False)
✅ 역할:
tokenizer: 텍스트를 숫자로 바꿔주는 모듈. 텍스트 → 토큰 → ID 변환 역할.use_fast=False: Python 기반의 tokenizer 사용을 의미해.
❓왜 use_fast=False?
- Fast tokenizer는 Rust로 구현된 고속 버전인데, 일부 모델(Qwen 등)은 구조가 특이해서 fast tokenizer가 잘 안 맞거나 형태소 분석 결과가 다르게 나올 수 있음.
- 따라서
Qwen계열 모델이나RoPE,ChatGLM같은 경우에는 정확한 토크나이징을 위해use_fast=False를 사용하는 게 안정적.
⚙️ 다른 옵션:
| 옵션 | 설명 |
|---|---|
use_fast=True (기본값) |
속도가 빠르고 대부분의 모델에서 사용 가능 |
trust_remote_code=True |
모델이 사용자 정의 코드(custom class)를 사용할 때 필요 |
revision="main" |
특정 버전(branch/tag)의 tokenizer 사용 |
2. AutoModelForCausalLM.from_pretrained(model_name)
✅ 역할:
- 모델 구조 및 사전 학습된 가중치를 로드함. 여기서는 CausalLM (Autoregressive LM) 용 구조.
Qwen2.5-0.5B-Instruct: GPT처럼 다음 단어를 생성하는 방식에 적합한 구조.
⚙️ 다른 옵션:
| 옵션 | 설명 |
|---|---|
trust_remote_code=True |
사용자 정의 모델 구조가 포함된 경우 필수 |
torch_dtype=torch.float16 |
메모리 절약을 위해 fp16으로 로드 |
device_map="auto" |
자동으로 GPU/CPU에 모델 분배 |
3. model.cuda()
✅ 역할:
- 모델을 GPU로 옮김.
- 학습 또는 추론 시 계산을 CUDA를 사용하는 GPU에서 수행하도록 함.
❓왜 필요한가?
- GPU는 병렬 연산에 강하므로, 학습 속도가 훨씬 빠르고 대규모 모델 처리에 필수.
.cuda()없으면 CPU로 동작하고 매우 느림.
⚠️ 주의:
- 모델이 너무 크면 GPU 메모리를 초과할 수 있음 → 이때는
model.to("cuda:1"),torch_dtype=torch.float16또는deepspeed,accelerate사용.
4. model.train()
✅ 역할:
- 모델을 training mode로 전환함.
- dropout, batchnorm 등 학습 중에만 동작해야 할 layer들이 활성화됨.
❓왜 필요한가?
eval()모드는 예측용 → dropout 등 꺼짐train()모드에서는 모델이 학습 중인 상태임을 PyTorch에 알려줘야 함
🧩 정리
| 코드 | 설명 | 왜 필요한가 |
|---|---|---|
use_fast=False |
느리지만 정확한 tokenizer 사용 | Qwen 계열은 fast tokenizer와 호환 이슈 있을 수 있음 |
model.cuda() |
모델을 GPU로 보냄 | 빠른 연산과 대규모 모델 처리 |
model.train() |
학습 모드 설정 | dropout 등 학습 중에만 작동해야 하는 layer 활성화 |
🔮 앞으로 참고할 수 있는 옵션 예시
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16, # FP16으로 더 적은 메모리 사용
trust_remote_code=True # Qwen처럼 custom 코드 포함된 모델 필수
)
model = model.to("cuda")
🔧 전체 코드 설명 요약
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
num_epochs = 1
num_training_steps = num_epochs * len(data)
lr_scheduler = get_scheduler(
name="linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps
)
1️⃣ Optimizer (최적화 알고리즘)
✅ 정의:
- 모델의
가중치(weight)를 어떻게 바꿔야 손실(loss)이 줄어들지 계산하고 업데이트하는 알고리즘. - 대표적인 방법은 경사 하강법(Gradient Descent) 계열.
📦 torch.optim.AdamW
AdamW는Adam에서 파생된 옵티마이저로,W는 weight decay를 decoupled하게 적용한다는 뜻이야.- 일반
Adam은 weight decay와 learning rate가 충돌할 수 있는데,AdamW는 이를 해결함.
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
⚙️ 주요 옵션
| 파라미터 | 설명 |
|---|---|
lr |
학습률 (learning rate): 얼마나 크게 가중치를 업데이트할지 |
betas |
(β1, β2) 모멘텀 계수로 이전 gradient의 영향을 얼마나 줄지 결정 (기본값 (0.9, 0.999)) |
eps |
수치 안정성을 위한 작은 값 |
weight_decay |
L2 regularization에 해당, overfitting 방지 |
✅ Optimizer 종류 비교
| 이름 | 특징 | 장점 | 단점 |
|---|---|---|---|
| SGD | 가장 기본적인 경사 하강법 | 간단, 계산량 적음 | 느리고 최적점에 도달 어려움 |
| SGD + Momentum | 과거 gradient 고려 | 빠르고 안정적 수렴 | 튜닝 어려움 |
| Adam | 모멘텀 + adaptive learning | 튜닝 적고 대부분 잘 작동 | 일반적인 L2 정규화와 충돌 가능 |
| AdamW | Adam 개선, L2 정규화 별도 적용 | LLM에서 가장 널리 사용됨 | 여전히 튜닝 필요 |
| RMSProp | 제곱된 gradient 평균 사용 | 비정상적인 loss에 안정적 | 덜 일반적으로 사용됨 |
2️⃣ Scheduler (학습률 조절기)
✅ 정의:
학습 중에 lr(learning rate)을 점점 줄이거나 늘려서 더 안정적으로 학습할 수 있게 도와주는 장치.
📦 get_scheduler(name="linear", ...)
HuggingFace의 transformers.optimization.get_scheduler 함수로,
학습률 스케줄을 쉽게 설정할 수 있음.
lr_scheduler = get_scheduler(
name="linear", # 선형 감소
optimizer=optimizer,
num_warmup_steps=0, # 학습 초기에 LR을 점점 증가시키는 구간
num_training_steps=... # 전체 학습 단계 수
)
📊 대표 스케줄러 종류
| 이름 | 설명 | 특징 |
|---|---|---|
linear |
warmup 후 선형 감소 | 가장 널리 쓰임 |
cosine |
코사인 곡선처럼 감소 | 후반에 급격히 줄지 않음 |
polynomial |
다항식으로 감소 | 학습률 미세 조정 가능 |
constant |
일정하게 유지 | 실험용 혹은 warmup-only 설정 |
constant_with_warmup |
warmup 후 일정 | transformer 초기 구조와 유사 |
🔁 Warmup이란?
- 학습 초반에 너무 큰
lr을 쓰면 성능이 불안정해짐 → 처음엔 천천히 시작 num_warmup_steps: 몇 step 동안 lr을 선형으로 키울지 정함
📌 이 조합의 장단점 정리
| 항목 | 장점 | 단점 |
|---|---|---|
AdamW |
대부분의 모델에서 잘 작동, 안정적 수렴, L2 정규화와 궁합 좋음 | 여전히 learning rate 튜닝 필요 |
Scheduler (linear) |
너무 크거나 작은 lr로 인한 불안정성 줄임 | 적절한 num_training_steps 계산 필요 |
Warmup |
학습 초반 과도한 업데이트 방지 | 너무 작으면 효과 없음, 너무 크면 비효율적 |
✅ 보너스: 학습 루프에 사용하는 예시
for epoch in range(num_epochs):
for batch in dataloader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
✨ 마무리
optimizer.step(): weight를 업데이트lr_scheduler.step(): 학습률을 업데이트zero_grad(): 이전 gradient를 초기화
✅ 1. nn.CrossEntropyLoss()란?
loss_fn = nn.CrossEntropyLoss()
🔧 역할:
- 모델이 예측한 확률 분포와 정답 레이블 간의 차이를 계산해 손실값(loss)을 반환함.
- 다중 클래스 분류에서 주로 사용됨.
🔍 작동 방식:
- 내부적으로
log_softmax+NLLLoss(Negative Log Likelihood) 조합임.
예시:
logits = torch.tensor([[2.0, 1.0, 0.1]]) # 모델 출력 (3개 클래스)
labels = torch.tensor([0]) # 정답 클래스 인덱스
loss = nn.CrossEntropyLoss()(logits, labels)
📌 2. LLM 학습에 사용되는 주요 손실 함수들
1) 🔹 CrossEntropyLoss (기본)
- 용도: 토큰 단위 next-token prediction
- 특징: 다중 클래스 분류에 적합
- 장점: 학습이 안정적, 널리 검증됨
- 단점: 단일 토큰 예측만을 목표로 하므로 복합적 표현 학습에 제약
2) 🔹 KL-Divergence Loss (nn.KLDivLoss)
- 용도: Knowledge Distillation (e.g. 작은 student 모델이 teacher 모델을 모방할 때)
- 특징: 두 확률 분포 간의 차이 계산
- 장점: soft label 학습 가능 → 부드러운 분포 반영
- 단점: 분포 간 0값이나 log 문제로 수치적 불안정 가능
3) 🔹 Cosine Embedding Loss
nn.CosineEmbeddingLoss(input1, input2, target)
- 용도: 임베딩 유사도 학습 (e.g., 문장 유사도)
- 특징: 두 벡터 간 코사인 유사도를 학습
- 장점: 표현 공간을 잘 정렬시켜줌 (semantic retrieval 등에서 강력)
- 단점: classification에는 직접적으로 부적합
4) 🔹 Contrastive Loss / InfoNCE
- 용도: dense retrieval, pretraining, encoder 모델 학습 등에서 positive/negative 쌍 구분
- 특징: anchor와 positive는 가깝게, negative는 멀게 학습
- 장점: 표현 학습 강화, downstream task에 잘 일반화됨
- 단점: negative 샘플 품질에 따라 성능 크게 좌우됨
5) 🔹 Perplexity (CrossEntropy의 지수 변환)
- 용도: LLM 평가용 (loss는 그대로 CrossEntropy 사용)
- 공식:
Perplexity = exp(CrossEntropy) - 해석: 낮을수록 모델이 덜 “놀람” → 더 좋은 모델
6) 🔹 Label Smoothing Loss
CrossEntropy + soft target (e.g., [0.9, 0.1] 대신 [0.7, 0.2, 0.1])
- 용도: 과적합 방지, 일반화 향상
- 특징: hard label 대신 soft한 분포 사용
- 장점: 노이즈에 강하고 overconfidence 방지
- 단점: 학습이 느려질 수 있음
7) 🔹 Language Modeling Loss (특화된 구성)
- Causal Language Modeling (CLM):
nn.CrossEntropyLosswith causal mask → GPT류 - Masked Language Modeling (MLM): BERT류 모델 →
CrossEntropy+ mask 적용 - SpanBERT, ELECTRA Loss: 다양한 pretraining objective 조합
🔍 표로 정리: LLM 학습에 활용 가능한 손실 함수 비교
| Loss 이름 | 사용 목적 | 장점 | 단점 |
|---|---|---|---|
| CrossEntropyLoss | Next-token 예측 | 안정적, 널리 사용 | 단일 토큰만 고려 |
| KLDivLoss | Knowledge Distillation | soft label 학습 가능 | 수치 불안정 |
| CosineEmbeddingLoss | 임베딩 유사도 | 표현 정렬에 효과적 | 분류에는 적합하지 않음 |
| Contrastive Loss | 표현학습, retrieval | 일반화 강함, 쌍 학습에 적합 | hard negative 중요 |
| Label Smoothing | 과적합 방지 | generalization 향상 | 확률 분포 왜곡 가능 |
| MLM Loss | Encoder pretrain | 다양한 문맥 학습 가능 | 복잡도 증가 |
| Perplexity (평가용) | 모델 성능 측정 | 해석 쉬움 | 실제 학습에는 사용 안 함 |
✅ 결론: 어떤 Loss를 언제 써야 할까?
| 상황 | 추천 Loss |
|---|---|
| GPT, Qwen 등 LLM 학습 | CrossEntropyLoss (Causal LM) |
| 임베딩 기반 QA, 검색 | Contrastive Loss, CosineEmbeddingLoss |
| Distillation | KLDivLoss, MSELoss |
| BERT pretrain | MLM + CrossEntropy |
| 학습 안정성 강화 | Label Smoothing 추가 |
🔍 전체 코드 다시 보기
tokens = tokenizer(sample["answer_text"], return_tensors="pt")
labels = tokens.input_ids.cuda() # [1, seq_len]
eos_id = tokenizer.eos_token_id
labels = torch.cat([labels, torch.tensor([[eos_id]], device=labels.device)], dim=1) # [1, seq_len+1]
optimizer.zero_grad()
seq_len = labels.size(1)
total_sample_loss = 0.0
🧩 각 줄별 핵심 개념과 설명
✅ tokenizer(..., return_tensors="pt")
tokens = tokenizer(sample["answer_text"], return_tensors="pt")
🧠 개념:
- 자연어 텍스트(문장)를 토큰 ID로 변환 → 모델이 이해할 수 있게 바꿔줌
return_tensors="pt"는 PyTorch 텐서로 반환해주는 옵션
❓왜 필요한가?
- 모델 입력과 정답은 모두 "숫자 시퀀스"여야 함 (ex:
[203, 105, 408, 2]) - 그래서 학습 전 반드시 토크나이저로 텍스트를 전처리해야 함
✅ labels = tokens.input_ids.cuda()
labels = tokens.input_ids.cuda() # [1, seq_len]
🧠 개념:
- 정답 토큰 시퀀스를 GPU로 이동시켜서 이후 연산에서 GPU를 사용할 수 있도록 함
💡 참고:
- 이후
model(input_ids)의 출력과 이labels를 비교하여 loss를 계산함
✅ eos_id = tokenizer.eos_token_id
eos_id = tokenizer.eos_token_id
🧠 개념:
</s>또는<|endoftext|>와 같은 "문장의 끝"을 나타내는 EOS (End Of Sentence) 토큰의 ID
❓왜 필요한가?
- LLM은 문장이 언제 끝나는지 알려줘야 적절한 위치에서 생성을 멈출 수 있음
- 정답 레이블의 끝에 이 EOS 토큰이 없으면, 모델은 문장이 계속 이어진다고 착각할 수 있음
✅ torch.cat([labels, eos_token], dim=1)
labels = torch.cat([labels, torch.tensor([[eos_id]], device=labels.device)], dim=1)
🧠 개념:
- 정답 시퀀스 끝에 EOS를 수동으로 추가함 →
[1, 2, 3] → [1, 2, 3, 0]
📌 왜 중요?
- autoregressive 모델은 한 토큰씩 예측하기 때문에, 마지막 토큰인 EOS도 예측 대상으로 넣어줘야 학습이 완성됨
✅ optimizer.zero_grad()
optimizer.zero_grad()
🧠 개념:
- 이전 배치에서 계산한 gradient를 초기화함
❓왜 필요한가?
- PyTorch는 기본적으로 gradient를 누적함
- 그래서 매 학습 step마다
zero_grad()로 gradient를 비워줘야 새로운 gradient만 계산됨
🔥 안 하면 생기는 문제:
- 이전 step의 gradient가 계속 누적되어 weight가 비정상적으로 업데이트됨
✅ seq_len = labels.size(1)
seq_len = labels.size(1)
- 예측해야 할 시퀀스 길이 계산 → 학습 루프에서 필요
✅ total_sample_loss = 0.0
- 해당 샘플의 총 loss 초기화 (loop 안에서 누적 예정)
🔄 요약 표
| 코드 | 의미 | 왜 필요한가 |
|---|---|---|
tokenizer(..., return_tensors="pt") |
텍스트 → 토큰 시퀀스 | 모델 입력/출력은 숫자여야 함 |
labels = input_ids.cuda() |
GPU에 올림 | 연산 속도 향상 |
eos_token 추가 |
문장 끝 표시 | 언제 멈춰야 하는지 학습함 |
optimizer.zero_grad() |
gradient 초기화 | 이전 배치의 gradient 누적 방지 |
seq_len 계산 |
토큰 개수 파악 | 루프, loss 계산 등에 사용 |
🔁 코드 전체 다시 보기
outputs = model(inputs_embeds=inputs_embeds,
use_cache=False,
output_hidden_states=True)
logits = outputs.logits[:, -1, :] # [batch_size, vocab_size]
target = labels[:, t] # 정답 토큰 (인덱스)
step_loss = loss_fn(logits, target) # CrossEntropyLoss 계산
total_sample_loss += step_loss # 누적
🧩 1. model(...inputs_embeds...)의 의미
📦 입력: inputs_embeds
- 일반적으로 모델은
input_ids(토큰 ID)를 받지만, - 이미 임베딩된 벡터 (
inputs_embeds)를 넣는다면 임베딩 레이어를 생략하고 바로 계산.
🔁 출력: outputs = model(...)
transformers계열 모델의 output은 일반적으로 다음과 같은ModelOutput객체:
BaseModelOutputWithPast(
logits=..., # 다음 토큰에 대한 분포
past_key_values=..., # 캐시 (안 씀)
hidden_states=..., # 모든 layer의 출력
attentions=... # attention score (옵션)
)
즉, 실제로 여러 정보가 동시에 출력됨.
🧩 2. logits = outputs.logits[:, -1, :]
✅ 설명:
logits: 모델이 예측한 "다음 토큰이 무엇일 확률 분포". shape:[batch_size, seq_len, vocab_size][:, -1, :]: 마지막 위치의 토큰 예측만 뽑음 → 즉, 가장 최근 입력 토큰 다음의 토큰을 예측
예시:
logits.shape = [1, 10, 32000] # 마지막 토큰 예측 (vocab 크기만큼 확률)
logits[:, -1, :] → shape: [1, 32000]
🧩 3. target = labels[:, t]
- 현재 시점
t에서의 정답 토큰 인덱스 (예:1351,209) labels는[batch_size, seq_len]형태의 토큰 ID 텐서target은 정수 값 하나 → CrossEntropyLoss에 입력될 정답 레이블
🧩 4. step_loss = loss_fn(logits, target)
📦 CrossEntropyLoss 동작 방식:
logits → [batch_size, vocab_size] → softmax → 확률 분포
target → 정답 클래스 인덱스
- 내부적으로 다음을 수행:
loss = -log(softmax(logits)[target])
✅ 결과:
step_loss는 스칼라 값 (float)- 이 시점에서 예측한 토큰의 정확도에 따라 loss가 작게 혹은 크게 나옴
🧩 5. total_sample_loss += step_loss
❓왜 누적만 하고 backward()를 안 하는가?
💡 이유 1: 토큰 단위 학습 루프 중이기 때문
- 이 구조는 전체 문장을 한꺼번에 학습하는 게 아니라,
- 한 문장에서 매 시점마다 토큰 하나씩 예측하는 auto-regressive loop
예시:
"오늘은 날씨가 좋다"
→ "오" → "늘" → "은" → ... 순차적으로 예측
→ 각 시점의 loss를 누적한 뒤 전체 문장 loss로 처리
💡 이유 2: 전체 문장 학습 단위로 backward() 수행 예정
- 토큰마다
backward()하면 너무 많은 메모리를 쓰고, 너무 느려짐 - 대신 마지막에 아래처럼 한 번만 처리:
total_sample_loss.backward()
optimizer.step()
📌 전체 흐름 요약
| 단계 | 코드 | 설명 |
|---|---|---|
| 입력 | inputs_embeds |
이미 임베딩된 입력을 모델에 전달 |
| 출력 | outputs.logits |
각 시점 다음 토큰 확률 분포 반환 |
| 선택 | [:, -1, :] |
마지막 토큰 위치의 예측 결과만 사용 |
| 정답 | labels[:, t] |
현재 시점의 정답 토큰 인덱스 |
| 계산 | CrossEntropyLoss(logits, label) |
예측 vs 정답 비교로 loss 계산 |
| 누적 | += step_loss |
전체 문장 loss를 계산하기 위해 누적 |
| 추후 | backward() |
전체 문장에 대한 loss로 역전파 예정 |
🔁 전체 코드 다시 보기
avg_sample_loss = total_sample_loss / seq_len
avg_sample_loss.backward()
optimizer.step()
lr_scheduler.step()
🔧 1. avg_sample_loss.backward()
✅ 역할:
- 역전파(Backward Propagation) 수행
- 손실(
loss)을 기준으로 모든 파라미터에 대해 gradient 계산
# 개념적으로는 다음과 같음:
∂loss / ∂W ← 각 layer의 가중치에 대해 미분
📌 주의:
- 이 단계에서 실제로 파라미터는 바뀌지 않음
- 단지
.grad속성에 gradient만 저장됨
🔧 2. optimizer.step()
✅ 역할:
.grad에 저장된 gradient 값을 이용해서 모델 파라미터를 업데이트
W = W - lr * ∂loss/∂W
- 여기서 쓰인 옵티마이저는
AdamW로, L2 regularization도 함께 적용함
🔧 3. lr_scheduler.step()
✅ 역할:
- 현재 학습 step을 기준으로 learning rate를 조정함
lr = scheduler.get_lr(step)
- 스케줄러는 보통:
- warmup 후 → 감소 (linear, cosine 등)
- 또는 cyclical / step decay 방식도 가능
📦 전체 흐름 정리
| 단계 | 함수 | 설명 |
|---|---|---|
| 1 | .backward() |
gradient 계산 (∂loss/∂weight) |
| 2 | optimizer.step() |
계산된 gradient로 weight 업데이트 |
| 3 | lr_scheduler.step() |
현재 step에 따라 learning rate 조정 |
✅ 평균 Loss로 나눈 것 맞을까?
avg_sample_loss = total_sample_loss / seq_len
왜 나누는가?
- auto-regressive loop에서
step_loss를 각 토큰마다 누적했으므로, 전체 문장 길이로 평균 내는 것은 일반적인 처리야. - 이렇게 하면 문장 길이에 상관없이
lossscale이 일정해져서 학습 안정화에 도움이 됨.
✅ 올바른 처리 방식이야.
🔀 대안 옵션들
1) gradient clipping (폭주 방지)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
- gradient가 너무 커서 weight가 이상하게 업데이트되는 것을 방지
2) optimizer.zero_grad() 호출 위치
- 보통은 loop의 시작 부분에서 호출해야 함
for ...:
optimizer.zero_grad() # gradient 초기화
loss.backward()
optimizer.step()
※ 만약 zero_grad()가 빠지면 gradient가 누적되어 학습이 망가짐
3) accumulate gradients (메모리 절약)
# 예: 4개 배치마다 한 번만 업데이트
if step % 4 == 0:
loss.backward()
optimizer.step()
optimizer.zero_grad()
→ 특히 큰 모델 학습 시 GPU 메모리가 부족할 때 많이 쓰는 기법
🧠 올바르게 작성되었는가?
| 항목 | 설명 | 문제 없음? |
|---|---|---|
avg_sample_loss.backward() |
gradient 계산 | ✅ OK |
optimizer.step() |
weight 업데이트 | ✅ OK |
lr_scheduler.step() |
learning rate 조정 | ✅ OK |
zero_grad() |
(루프 시작에 있어야 함) | ❗ 확인 필요 |
💡 주의: 이 코드 바로 앞에
optimizer.zero_grad()가 있어야 해!
✨ 전체적으로 정리
optimizer.zero_grad() # 🔁 1. 이전 gradient 초기화
total_sample_loss = 0
for t in range(seq_len):
...
step_loss = loss_fn(logits, target)
total_sample_loss += step_loss
avg_sample_loss = total_sample_loss / seq_len # 🔁 2. 토큰 평균
avg_sample_loss.backward() # 🔁 3. gradient 계산
optimizer.step() # 🔁 4. 파라미터 업데이트
lr_scheduler.step() # 🔁 5. 학습률 업데이트
728x90
'인공지능 > 공부' 카테고리의 다른 글
| mteb 한글 평가하기 (0) | 2025.08.29 |
|---|---|
| Multi-GPU 기본 개념 DDP, Data Parallel, Model Parallel (4) | 2025.07.30 |
| Collator란? (4) | 2025.07.29 |
| 딥러닝 총 정리 (2) | 2025.07.04 |
| 딥러닝 공부하기 3 (4) | 2025.07.03 |