Metadata-Version: 2.4
Name: rnjswldbf_2014
Version: 0.1.1
Summary: A reinforcement learning / supervised learning library implemented in D, exposed as a Python extension
Home-page: https://github.com/rnjswldbf2014-hash/ml
Author: Jeeyul Kwon
License: GPL-2.0
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# dplayg

D 언어로 구현한 머신러닝 라이브러리(`ml`)와 다목적 수학 자료형 라이브러리(`type`) 프로젝트입니다.
`ml` 디렉토리 내의 `ml.d`를 빌드하면 파이썬 확장 모듈인 `ml.pyd`가 생성되며, 파이썬에서 사용할 수 있습니다.
`type` 디렉토리 내에는 가변 정밀도 실수, 10진 부동소수점, 분수, Posit 등을 지원하는 `type.d`가 포함되어 있습니다.

---

## 설계 원칙

이 라이브러리의 함수는 **데이터를 만드는 함수** 와 **모델을 바꾸는 함수** 로 나뉩니다.

| 함수 | 순수? | 하는 일 |
|---|---|---|
| `rl()` | 순수 | `(입력, 출력)` 데이터만 만듭니다. 가중치를 건드리지 않습니다. |
| `reward()` | 순수 | `(입력, 출력, 보상)` 데이터만 만듭니다. |
| `episode()` | 순수 | 여러 스텝에 같은 보상을 매깁니다. |
| `predict()` | 순수 | 샘플링 없이 최선의 값만 봅니다. |
| **`save()`** | **아님** | **역전파 + 파일 저장. 모델이 바뀌는 유일한 곳입니다.** |

`rl()` 을 몇 번을 부르든 모델은 그대로입니다. 학습은 `save()` 를 부를 때만 일어납니다.

## 출력은 항상 리스트

출력이 하나든 여럿이든 **모양이 같습니다.** 출력 개수를 바꿔도 코드가 안 깨집니다.

```python
from ml import ml

ai = ml.make("M", [12, 128], [["왼쪽", "오른쪽"]])       # 하나여도 감쌈

step = ai.rl(입력)
행동 = step.output[0]                                 # 항상 [0]

ai.save(ai.reward(step, [1.0]))                       # 점수도 리스트
ai.sl(입력, ["왼쪽"])                                  # 정답도 리스트
ai.rl(입력, legal=[["왼쪽"]])                          # legal 도 리스트
```

출력을 늘리면 자리만 늘어납니다.

```python
ai = make("M", [12, 128], [["왼쪽", "오른쪽"], cos])

행동, 파워 = ai.rl(입력).output
ai.save(ai.reward(step, [1.0, None]))                 # None = 그 출력은 학습 제외
ai.sl(입력, ["왼쪽", 2.5])
```

---

## 모델 만들기

```python
from my_ml import make

from my_ml import make, cos

ai = make("MyModel", [12, 128, 128], [["왼쪽", "오른쪽", "정지"]])
```

| 인자 | 설명 |
|---|---|
| 1번째 | 모델 이름. 가중치 파일명(`이름_ml_memory.pth`)에 씁니다. |
| 2번째 | 레이어 구조 `[입력수, 은닉...]`. 은닉층은 몇 개든 됩니다. 출력은 여기 안 씁니다. `attn(n)` 을 끼우면 어텐션 층이 됩니다. |
| 3번째 | 출력 스펙. **항상 리스트**입니다. 하나여도 `[["A","B"]]` 처럼 감쌉니다. |
| `optimizer` | `'adam'`(기본) / `'sgd'` / `'rmsprop'` / `'adagrad'` |
| `sigma` | cos 가 값을 얼마나 넓게 탐험할지 (기본 1.0) |
| `entropy` | 고르는 쪽이 한 가지 답으로 굳는 것을 막는 힘 (기본 0.01) |

같은 이름의 파일이 있으면 이어서 학습합니다.
단, 저장된 구조가 요청한 `layers` 와 다르면 파일을 무시하고 새로 만듭니다.

---

## 강화학습

```python
step   = ai.rl([0.1, 0.2, ...])        # → Step(input, output)
scored = ai.reward(step, +1.0)         # → Scored(input, output, point)
ai.save(scored)                        # 학습 + 저장
```

`step.input` / `step.output` 으로 꺼내 쓰고, 언패킹도 됩니다.

```python
입력, 출력 = ai.rl([0.5])
```

### 묶어서 학습 (권장)

한 번에 여러 스텝을 넘기면 그만큼 빨라집니다.

```python
batch = []
for i in range(32):
    step = ai.rl([i / 32])
    point = +1.0 if step.output == "왼쪽" else -1.0
    batch.append(ai.reward(step, point))
ai.save(batch)
```

### 에피소드 단위 보상

게임이 끝난 뒤 결과를 알 때 씁니다.

```python
steps = [ai.rl(상태) for 상태 in 게임진행()]
ai.save(ai.episode(steps, +1.0 if 이겼으면 else -1.0))
```

### 고를 수 있는 것이 제한될 때

```python
step = ai.rl(상태, legal=["왼쪽", "정지"])   # 이 중에서만 고릅니다
```

---

## 숫자 출력 (cos)

고를 이름 대신 `cos` 를 넘기면 실수를 반환합니다.

```python
ai = make("NumModel", [2, 64], [cos])

step = ai.rl([0.5, 0.5])
print(step.output[0])                    # 예: 1.63

오차 = abs(step.output[0] - 3.0)
ai.save(ai.reward(step, [1.0 - 오차]))   # 3 에 가까울수록 높은 점수
```

값은 **0 근처에서 시작합니다.** 원하는 범위가 따로 있으면 쓰는 쪽에서 펼쳐 쓰세요.

```python
파워 = 20 + step.output[0] * 10   # 출력 -1~1 → 파워 10~30
```

---

## 출력 여러 개

레이어 마지막을 리스트로 주면 출력이 여러 갈래가 됩니다.
출력끼리 몸통 신경망을 공유합니다.

```python
ai = make("Game", [38, 128, 128],
          [["점프", "왼쪽", "오른쪽", "정지"], cos])

step = ai.rl(상태)
행동, 파워 = step.output          # ('왼쪽', 27.3)
```

고르기와 `cos` 를 자유롭게 섞을 수 있고, 개수 제한도 없습니다.

```python
make("M", [입력, 은닉, 은닉], [cos, cos, ["안녕", "친구들"], ["영어", "한국어"]])
```

### 출력마다 다른 보상

```python
ai.save(ai.reward(step, [행동점수, 파워점수]))
```

`None` 을 주면 그 출력은 이번 학습에서 빠집니다.
점프를 안 한 순간에 점프 파워를 학습시키면 안 되니까요.

```python
ai.save(ai.reward(step, [점수, 점수 if 점프했나 else None]))
```

### 출력마다 고를 수 있는 것 제한

```python
ai.rl(상태, legal=[["왼쪽", "정지"], None])
```

---

## 어텐션 층

은닉층 자리에 `attn(항목수)` 를 넣으면, 그 층에서 **항목끼리 서로 참조**합니다.

```python
from my_ml import make, attn, each

ai = make("M", [12, attn(6), each(24), attn(6), each(24), 128], [["A", "B"]])
```

`attn(6)` 은 들어온 폭을 6조각으로 나눠, 어느 조각을 볼지 스스로 정합니다.
폭은 그대로 나옵니다.

## each — 항목마다 따로 도는 층

`attn` 은 항목끼리 섞기만 하고 가공은 못 합니다. 그 가공을 하는 것이 `each` 입니다.

```python
each(24)     # 항목 하나를 24칸으로. 가중치는 하나를 모든 항목이 나눠 씀
```

**일반 층을 쓰면 안 됩니다.** 일반 층은 전체를 한 덩어리로 섞어서 항목 구분이
사라집니다. `attn` 이 만들어둔 정보가 거기서 없어집니다.

| 층 | 하는 일 |
|---|---|
| `attn(n)` | 항목끼리 서로 참조 (가로로 섞음) |
| `each(폭)` | 항목마다 따로 가공, 가중치 공유 (세로로 처리) |
| 일반 층 | 전부 합침 — 항목 구분 끝 |

보통 `attn` 과 `each` 를 번갈아 쌓고, 마지막에 일반 층으로 합칩니다.

### 실제 차이

항목 6개 중 **값이 가장 큰 것의 태그**를 맞히는 문제입니다. 순서가 매번 바뀌어서
"몇 번째 자리"로는 못 풉니다.

| | 정확도 |
|---|---|
| 일반 층만 | 87.8% |
| `attn` 만 | 87.5% |
| `attn` + `each` | **98.8%** |

`attn` 만 넣으면 소용이 없고, `each` 와 같이 써야 효과가 납니다.

### 파라미터도 훨씬 적습니다

| | 개수 |
|---|---|
| 일반 층 128→256 | 32,768 |
| `each(32)` (항목 8개 공유) | 512 |

### 언제 쓰나

입력에 **같은 종류가 여러 개** 있고, 그 중 뭐가 중요한지가 상황마다 바뀔 때입니다.
적 여러 명, 아이템 목록, 맵 칸 같은 경우요.

그냥 서로 다른 값들(체력, 속도, 거리)뿐이면 효과가 없고 계산만 늘어납니다.

**비용** — 항목 수의 제곱에 비례합니다.

`each` 는 앞에 `tok` 이나 `attn` 이 있어야 항목 수를 알 수 있습니다.

---

## 번호를 입력으로 줄 때

글자, 직업, 아이템 종류처럼 **크기에 뜻이 없는 번호**는 그대로 넣으면 안 됩니다.
신경망이 5를 10의 절반으로 보기 때문입니다.

번호마다 자리를 하나씩 만들고, 자기 자리만 1로 켜서 넣습니다 (one-hot).

```python
def 켜기(번호, 종류수):
    v = [0.0] * 종류수
    v[번호] = 1.0
    return v

ai = make("M", [33, 128, 128], [답목록])
ai.sl(켜기(5, 33), ["정답"])
```

여러 개면 이어붙입니다.

```python
ai = make("M", [16*33, 256], [글자목록])          # 글자 16개
ai.sl(켜기(a,33) + 켜기(b,33) + ..., ["정답"])
```

번호를 그대로 넣는 것과 차이가 큽니다. 번호 33개에 답이 무작위로 붙은 문제에서:

| | 정확도 |
|---|---|
| 번호 그대로 | 55% |
| 0~1 로 정규화 | 58% |
| one-hot | **100%** |

---

## 묶음 학습

`sl()` 에 여러 개를 겹쳐 주면 한 번에 처리합니다. **훨씬 빠릅니다.**

```python
ai.sl(입력, 정답)                                   # 하나씩
ai.sl([입력1, 입력2, ...], [정답1, 정답2, ...])      # 묶음
```

하나씩 부르면 문제마다 가중치 전체를 갱신합니다. 파라미터가 20만 개면
순전파보다 그 갱신이 더 비쌉니다. 묶음으로 주면 기울기를 모았다가 **한 번만**
갱신합니다.

| 32,000문제 | 시간 | 정확도 |
|---|---|---|
| 하나씩 | 143초 | 21% |
| 묶음 32 | 12초 | 84% |

묶음 크기는 직접 정합니다. 크게 잡으면 빠르지만 갱신 횟수가 줄어듭니다.

---

## 지도학습

정답을 알고 있을 때 씁니다.

```python
ai.sl([0.1, 0.2, 0.3], ["왼쪽"])  # 정답을 주면 학습하고 예측을 반환
ai.sl([0.1, 0.2, 0.3])           # 정답이 없으면 예측만
```

출력이 여러 개면 정답도 하나씩 줍니다. `None` 은 건너뜁니다.

```python
ai.sl(상태, ["점프", 1.0])        # 첫 출력 정답, 둘째 출력 목표값
ai.sl(상태, ["왼쪽", None])       # 둘째 출력은 학습 안 함
```

## 예측만 하기

학습도 샘플링도 없이, 가장 점수가 높은 것을 반환합니다.

```python
ai.predict([0.1, 0.2, 0.3])
```

---

## 그 밖에

```python
from my_ml import gc_disable, gc_collect

gc_disable()    # 학습 루프 전. D GC 를 멈춰 중간 끊김을 없앱니다.
...
gc_collect()    # 학습 후. GC 재개 + 수집.

resset("MyModel")   # 저장된 가중치 파일 삭제
```

---

## 예전 버전에서 넘어오기

v0.1 과 v0.2 는 API 가 호환되지 않습니다. 예전 코드는 그대로 돌아가지 않습니다.
예전 버전이 필요하면 `git checkout v0.1` 로 돌아갈 수 있습니다.

### 가중치 파일 변환

```python
from my_ml import change

change("MyModel")     # MyModel_ml_memory.pth 를 새 포맷으로
```

원본은 `.bak` 으로 남습니다. 이미 새 포맷이면 아무것도 하지 않습니다.
예전에 출력을 여러 개 썼다면 그대로 다 보존됩니다.

### 바뀐 것

| v0.1 | v0.2 |
|---|---|
| `make("M", ["A","B"], hidden_layers=[128])` | `make("M", [입력수, 128], [["A", "B"]])` |
| `chosen = ai.rl(acts, 체력, 라운드)` | `step = ai.rl([체력, 라운드])` |
| `ai.reward(+1)` — 여기서 학습 | `ai.save(ai.reward(step, +1))` — 여기서 학습 |
| `with ai.episode():` | `ai.episode(steps, 점수)` |
| `ai.save()` | `ai.save(scored)` — 인자 필수 |
| `ai.step()`, `ai.last_reward()` | 없어짐 |
| `make(..., reset=True)` | 없어짐 (`resset("M")` 로 파일 삭제) |

`reward()` 는 이름이 그대로지만 **더 이상 학습하지 않습니다.** 데이터만 만듭니다.
예전 코드가 에러 없이 조용히 학습만 안 되는 상태가 되니 주의하세요.

---

## 빌드

```powershell
$ldc   = "ldc2\ldc2-1.42.0-windows-x64\bin\ldc2.exe"
$pylib = "$env:LOCALAPPDATA\Programs\Python\Python313\libs\python313.lib"
& $ldc my_ml.d $pylib --O3 --release --shared --link-defaultlib-shared=false "-of=my_ml.pyd"
Remove-Item my_ml.obj, my_ml.lib, my_ml.exp -ErrorAction SilentlyContinue
```

`"-of=my_ml.pyd"` 의 따옴표는 필수입니다. 빼면 PowerShell 이 인자를 쪼개
`Error: unrecognized file extension pyd` 로 실패합니다.

---

## 성능

배치 1 온라인 학습(이 라이브러리의 주 용도)에서는 파이토치보다 빠릅니다.
직접 작성한 네이티브 파이토치 코드와 같은 조건으로 비교한 결과입니다.

| 구성 | 이 라이브러리 | 네이티브 PyTorch |
|---|---|---|
| `[1, 128, 3]` | **9.0 µs** | 707 µs |
| `[64, 128, 3]` | **91 µs** | 1,093 µs |
| `[256, 512, 3]` | 1,284 µs | **1,161 µs** |

망이 커지면(대략 13만 파라미터 이상) 파이토치가 유리해집니다.
배치 학습이 가능한 작업이라면 파이토치 쪽이 샘플당 훨씬 빠릅니다.
