결과 분석과 주장의 범위

먼저 읽을 장 23.5 실험 설계

실험이 끝나면 숫자가 남는다. "새 방법의 평균 오차가 0.11 줄었다"는 숫자는 출발점일 뿐이다. 어느 조건에서 줄었고 어느 조건에서 늘었는가, 그 차이가 seed와 표본의 우연보다 큰가, 그 차이를 만든 것이 정말 제안한 아이디어인가, 아니면 더 큰 모델과 더 많은 튜닝인가. 이 질문에 답하지 않고 쓴 주장은 리뷰에서 무너지거나, 더 나쁘게는 통과한 뒤 다른 사람의 실험에서 무너진다.

이 장은 결과를 주장으로 바꾸는 과정을 다룬다. 통계 도구(신뢰구간, 부트스트랩, 다중 비교, 짝지은 비교)는 12.4장과 12.5장에서 이미 다뤘다. 여기서는 그 도구를 써서 무엇을 주장할 수 있는지, 그 주장이 어디까지 성립하는지를 정하는 판단에 집중한다.

학습 목표

  • 결과를 오차 분석으로 해석한다
  • 주장의 범위를 근거에 맞춘다
  • 부정적 결과를 활용한다

오차 분석

왜 필요한가. 평균 하나는 여러 조건의 결과를 섞어 놓은 값이다. 평균이 좋아져도 어떤 조건에서는 나빠졌을 수 있고, 그 조건이 하필 사용자가 관심 있는 조건일 수 있다. 오차 분석(error analysis)은 평균을 조건별·샘플별로 쪼개 어디서 이기고 어디서 지는지를 찾는 일이다.

정의. 오차 분석은 두 층으로 한다.

  • 조건별 분해: 시험 데이터를 실험 조건(온도, 재료 계열, 관측소, 환자군, 입력 크기)으로 나누고 조건마다 지표와 두 방법의 차이를 계산한다. 조건은 결과를 보기 전에 정해 두는 것이 원칙이다(23.5장의 실험 계획서). 결과를 본 뒤 나쁜 조건만 골라 빼는 것은 오차 분석이 아니라 선택 편향이다.
  • 샘플별 분석: 오차가 가장 큰 샘플들을 직접 열어 본다. 레이블 오류, 측정 장비 이상, 분포 밖 입력, 모델이 체계적으로 틀리는 패턴(특정 범위의 값을 늘 과소 추정)이 여기서 드러난다. 숫자가 아니라 정성적 관찰이지만, 다음 실험의 가설을 만드는 가장 좋은 원천이다.

직관. 두 방법을 같은 시험 샘플에서 평가했다면 샘플마다 차이 di=eB,i−eA,id_i = e_{B,i} - e_{A,i}를 계산할 수 있다(ee는 절대오차, 음수면 B가 낫다). 전체 평균 dˉ\bar{d}는 조건별 평균 dˉc\bar{d}_c의 가중 평균이다.

dˉ=∑cncn dˉc.\bar{d} = \sum_c \frac{n_c}{n}\,\bar{d}_c .

여기서 ncn_c는 조건 cc의 샘플 수, nn은 전체 샘플 수다. 샘플이 많은 조건이 평균을 지배한다. 샘플이 적은 조건에서 큰 손해를 봐도 전체 평균은 좋아질 수 있다.

코드. 배터리 셀 440개의 SOH(state of health) 예측 오차를 시험 온도 세 조건으로 나눈다. 시뮬레이션에서 새 모델 B는 25 °C와 45 °C에서는 A보다 낫고, 0 °C에서는 A보다 나쁘다. 0 °C 셀은 60개뿐이다.

import numpy as np

rng = np.random.default_rng(8)
n_by_cond = {"0 °C": 60, "25 °C": 300, "45 °C": 80}       # 조건별 시험 셀 수
true_gap = {"0 °C": +0.30, "25 °C": -0.25, "45 °C": -0.12}  # 참 차이 B − A (%p)
cond, err_a, err_b = [], [], []
for c, n in n_by_cond.items():
    hard = rng.gamma(2.0, 0.6, size=n)                       # 셀마다 공통 난이도, (n,)
    err_a.append(np.abs(hard + rng.normal(0, 0.4, n)))      # A의 절대오차 (%p)
    err_b.append(np.abs(hard + true_gap[c] + rng.normal(0, 0.4, n)))
    cond += [c] * n
cond, err_a, err_b = np.array(cond), np.concatenate(err_a), np.concatenate(err_b)   # (440,)
d = err_b - err_a                                            # 짝지은 차이, 음수면 B가 낫다

print(f"전체   n={len(d):3d}  A {err_a.mean():.3f}  B {err_b.mean():.3f}  B−A {d.mean():+.3f} %p")
for c in n_by_cond:
    m = cond == c
    print(f"{c:6s} n={m.sum():3d}  A {err_a[m].mean():.3f}  B {err_b[m].mean():.3f}"
          f"  B−A {d[m].mean():+.3f} %p  B가 나은 셀 {np.mean(d[m] < 0):.0%}")
전체   n=440  A 1.246  B 1.134  B−A -0.112 %p
0 °C   n= 60  A 1.311  B 1.582  B−A +0.270 %p  B가 나은 셀 37%
25 °C  n=300  A 1.240  B 1.054  B−A -0.186 %p  B가 나은 셀 63%
45 °C  n= 80  A 1.223  B 1.099  B−A -0.124 %p  B가 나은 셀 55%

전체 평균만 보면 B가 0.112 %p 낫다. 조건별로 보면 0 °C에서는 B가 0.270 %p 나쁘고, 0 °C 셀 중 B가 나은 것은 37%뿐이다. 0 °C 셀이 전체의 14%라서 평균에서 묻혔다. 또 B가 크게 이기는 25 °C에서도 B가 나은 셀은 63%다. "평균적으로 낫다"는 것은 "모든 셀에서 낫다"는 뜻이 아니다.

연구에서의 의미. 조건별 분해 표는 논문의 주장을 정하는 재료다. 위 결과로는 "B가 A보다 낫다"가 아니라 "B는 상온·고온에서 낫고 저온에서는 나쁘다"를 써야 한다. 저온에서 나빠지는 이유를 샘플별 분석으로 찾으면(예: 저온 데이터가 학습 세트에 적었다, 저온에서 내부 저항이 커지는 물리를 모델이 못 배웠다), 그것이 다음 연구 질문이 된다.

주장의 범위

왜 필요한가. 오차 분석으로 조건별 차이를 얻었다. 그 차이가 우연(표본과 seed의 변동)보다 큰지, 그리고 여러 조건에 대한 주장을 동시에 할 때 우연히 하나쯤 틀릴 확률이 얼마인지를 따져야 주장의 범위가 정해진다.

정의. 주장의 범위는 세 축으로 적는다.

  • 데이터: 어떤 데이터셋, 어떤 분포에서 측정했는가. 학습 분포 안인가 밖인가.
  • 조건: 어떤 실험 조건(온도, 크기, 해상도)에서 성립하는가. 측정하지 않은 조건은 범위 밖이다.
  • 지표: 어떤 metric에서 성립하는가. MAE에서 이기고 최대 오차에서 지는 일은 흔하다.

그리고 각 주장에는 판정이 붙는다. 이 장에서는 짝지은 차이의 95% 구간으로 판정한다. 구간 전체가 0보다 작으면 "B 우세", 0보다 크면 "A 우세", 0을 포함하면 "판정 보류"다. 판정 보류는 "두 방법이 같다"는 뜻이 아니다. 이 데이터로는 방향을 말할 수 없다는 뜻이다.

코드. 각 시험 셀이 독립인 개체라고 보고 셀을 복원추출하는 짝지은 부트스트랩으로 조건별 구간을 만든다(부트스트랩과 재표본화 단위는 12.4장).

def paired_boot(d, B=4000, seed=1):
    r = np.random.default_rng(seed)
    idx = r.integers(0, len(d), size=(B, len(d)))            # 셀을 복원추출, (B, n)
    return np.percentile(d[idx].mean(axis=1), [2.5, 97.5])

def verdict(lo, hi):
    return "B 우세" if hi < 0 else "A 우세" if lo > 0 else "판정 보류"

for name, m in [("전체", np.ones_like(d, bool))] + [(c, cond == c) for c in n_by_cond]:
    lo, hi = paired_boot(d[m])
    print(f"{name:6s} B−A {d[m].mean():+.3f}  95% 구간 [{lo:+.3f}, {hi:+.3f}]  → {verdict(lo, hi)}")
전체     B−A -0.112  95% 구간 [-0.162, -0.060]  → B 우세
0 °C   B−A +0.270  95% 구간 [+0.117, +0.420]  → A 우세
25 °C  B−A -0.186  95% 구간 [-0.246, -0.127]  → B 우세
45 °C  B−A -0.124  95% 구간 [-0.239, -0.016]  → B 우세

조건 하나하나를 따로 보면 세 판정이 모두 분명하다. 그런데 논문에서는 이 셋을 한꺼번에 주장한다. 각 구간의 오류 확률이 5%이면, 세 주장 중 적어도 하나가 틀릴 확률은 5%보다 크다(12.4장의 가족별 오류율). 그래서 조건별 주장을 여러 개 할 때는 다중 비교 보정을 한다.

Holm 보정. Bonferroni는 모든 검정을 α/k\alpha/k로 하므로 보수적이다. Holm(1979)의 순차 기각 절차는 같은 가족별 오류율 보장을 유지하면서 더 많이 기각한다. p-value를 작은 것부터 p(1)≤p(2)≤⋯≤p(k)p_{(1)} \le p_{(2)} \le \dots \le p_{(k)}로 정렬하고

p(j)≤αk−j+1p_{(j)} \le \frac{\alpha}{k - j + 1}

을 j=1j = 1부터 차례로 확인한다. 만족하면 그 가설을 기각하고 다음으로 넘어가며, 처음 만족하지 못하는 순간 멈추고 나머지는 모두 기각하지 않는다. 첫 단계의 기준은 Bonferroni와 같은 α/k\alpha/k이고, 하나를 기각할 때마다 기준이 α/(k−1)\alpha/(k-1), α/(k−2)\alpha/(k-2)로 느슨해진다. 이미 기각한 가설은 "남은 가설 중에서" 오류를 낼 수 없다는 논리다. Holm 절차는 검정들 사이의 의존성과 무관하게 가족별 오류율을 α\alpha 이하로 제어한다.

from scipy import stats

names = list(n_by_cond)
p = np.array([stats.ttest_rel(err_b[cond == c], err_a[cond == c]).pvalue for c in names])
alpha, k = 0.05, len(p)
order = np.argsort(p)                                        # 작은 p부터
holm = np.zeros(k, bool)
for rank, i in enumerate(order):                             # rank = 0, 1, 2
    if p[i] > alpha / (k - rank):
        break                                                # 처음 실패하면 멈춘다
    holm[i] = True
for i, c in enumerate(names):
    print(f"{c:6s} p = {p[i]:.1e}  보정 없음 {p[i] < alpha!s:5s}  "
          f"Bonferroni {p[i] < alpha / k!s:5s}  Holm {holm[i]!s:5s}")
0 °C   p = 7.8e-04  보정 없음 True   Bonferroni True   Holm True
25 °C  p = 1.8e-09  보정 없음 True   Bonferroni True   Holm True
45 °C  p = 3.7e-02  보정 없음 True   Bonferroni False  Holm True

45 °C의 p-value 0.037은 Bonferroni 기준 0.05/3≈0.01670.05/3 \approx 0.0167을 넘지 못한다. Holm에서는 앞의 두 가설이 먼저 기각되어 세 번째 기준이 0.05/1=0.050.05/1 = 0.05가 되므로 기각된다. 세 조건의 판정을 동시에 주장해도 가족별 오류율 5%가 유지된다. 다만 45 °C의 효과는 가장 약한 근거 위에 있다. 구간의 위쪽 끝이 −0.016으로 0에 가깝고, 셀이 80개뿐이다. 이런 주장은 "45 °C에서도 개선이 관찰되었다(효과 크기의 불확실성이 크다)"처럼 근거의 강도를 함께 쓴다.

연구에서의 의미. 주장의 범위를 정하는 가장 실용적인 도구는 주장–근거 표다. 논문에 쓸 문장마다 한 줄씩, 근거 실험·데이터·조건·지표·구간·판정을 적는다. 이 장의 예라면 다음과 같다.

주장 근거 실험 조건 지표 B − A, 95% 구간 판정
B는 상온에서 A보다 정확하다 시험 셀 300개, 짝지은 부트스트랩 25 °C MAE (%p) −0.186 [−0.246, −0.127] B 우세 (Holm 보정)
B는 고온에서도 개선된다 시험 셀 80개 45 °C MAE (%p) −0.124 [−0.239, −0.016] B 우세, 근거 약함
B는 저온에 쓰지 않는다 시험 셀 60개 0 °C MAE (%p) +0.270 [+0.117, +0.420] A 우세
(주장하지 않음) 다른 화학조성에서의 성능 없음 — — — 범위 밖

표에 근거가 없는 행이 있으면 그 주장은 쓰지 않거나 추가 실험을 한다. 마지막 행처럼 하지 않는 주장을 적어 두면 논문의 한계 절을 쓸 때 그대로 옮길 수 있다(23.8장).

대안 설명

왜 필요한가. B가 A보다 낫다는 것이 확실해도, 왜 나은지는 별개의 문제다. 연구자는 자기 아이디어 덕분이라고 믿고 싶지만, 같은 결과를 설명하는 다른 가설이 있으면 그 믿음은 근거가 없다. Lipton과 Steinhardt(2018)는 ML 논문에서 흔한 문제로 "실험적 이득의 출처를 밝히지 않는 것"을 꼽았다. 성능 향상이 실제로는 제안한 구조가 아니라 튜닝이나 다른 변경에서 왔는데, 논문은 구조 덕분이라고 쓰는 경우다.

흔한 대안 설명. 새 방법이 이겼을 때 먼저 의심할 목록이다.

  • 용량(capacity): 새 모델의 파라미터 수·계산량이 더 크다.
  • 튜닝 예산: 새 방법은 하이퍼파라미터를 50번 탐색했고 baseline은 기본값이다.
  • 부수 변경: 새 방법과 함께 학습률 스케줄·데이터 증강·정규화·학습 epoch를 바꿨다.
  • 누수: 새 방법의 전처리가 시험 데이터 정보를 썼다(15.4장).
  • 우연: seed·분할의 변동(12.5장).
  • 평가 차이: 두 방법이 다른 시험 세트·다른 metric 구현으로 평가되었다.

배제하는 방법. 대안 설명 하나마다 그것만 바꾸는 실험을 설계한다. 용량이 의심되면 baseline을 같은 파라미터 수로 키우고, 튜닝 예산이 의심되면 baseline에도 같은 예산을 주고, 부수 변경이 의심되면 그 변경을 baseline에 적용한다. 제안한 구성 요소 각각의 기여는 ablation(구성 요소를 하나씩 빼고 다시 학습)으로 잰다.

ablation 표 읽기. ablation 표를 읽을 때 확인할 것은 세 가지다. 각 행의 차이가 seed 변동보다 큰가, 차이를 같은 seed끼리 짝지어 계산했는가, baseline을 공정하게 키운 행이 있는가. seed 5개로 학습한 다섯 모델을 비교한다. "A 확대"는 baseline A를 B와 같은 파라미터 수로 키운 것이다.

t4 = stats.t.ppf(0.975, df=4)                                # 자유도 4의 97.5% 분위수
seed_fx = rng.normal(0, 0.04, size=5)                        # seed마다 공통 효과 (분할·초기화), (5,)
true_rmse = {"B 전체": 1.00, "− 물리 손실": 1.12, "− attention": 1.01,
             "A (baseline)": 1.20, "A 확대": 1.09}       # A 확대: B와 같은 파라미터 수
runs = {m: v + seed_fx + rng.normal(0, 0.02, 5) for m, v in true_rmse.items()}   # 각 (5,)
full = runs["B 전체"]
for name, r in runs.items():
    diff = r - full                                          # 같은 seed끼리 짝지은 차이, (5,)
    half = t4 * diff.std(ddof=1) / np.sqrt(5)                # 95% t 구간의 반폭
    gap = "" if name == "B 전체" else f"B 전체 대비 {diff.mean():+.3f} ± {half:.3f}"
    print(f"{name:13s} RMSE {r.mean():.3f} ± {r.std(ddof=1):.3f}   {gap}".rstrip())
B 전체          RMSE 0.998 ± 0.040
− 물리 손실       RMSE 1.117 ± 0.067   B 전체 대비 +0.119 ± 0.051
− attention   RMSE 1.007 ± 0.042   B 전체 대비 +0.010 ± 0.044
A (baseline)  RMSE 1.198 ± 0.042   B 전체 대비 +0.200 ± 0.019
A 확대          RMSE 1.079 ± 0.055   B 전체 대비 +0.082 ± 0.029

RMSE 열의 "±"는 seed 5개의 표준편차이고, 오른쪽 열의 "±"는 짝지은 차이의 95% 구간 반폭이다. 읽는 법은 다음과 같다.

  • 물리 손실은 기여한다. 빼면 RMSE가 0.119 ± 0.051 늘어난다. 구간이 0을 포함하지 않는다.
  • attention의 기여는 보이지 않는다. 빼도 0.010 ± 0.044로 구간이 0을 포함한다. "attention이 성능을 높인다"는 주장은 이 근거로 할 수 없다. 단, "기여가 없다"고 주장할 수도 없다. 구간이 ±0.044 범위의 효과를 배제하지 못한다.
  • 이득의 상당 부분은 용량이다. B와 A의 차이 0.200 중 A를 같은 크기로 키우면 0.082만 남는다. 이득의 약 60%는 모델 크기로 설명된다. 논문에는 B 대 A가 아니라 B 대 A 확대의 차이(0.082)를 제안 방법의 기여로 써야 한다.
  • seed 사이 변동이 공통이다. RMSE 열의 표준편차(0.04–0.07)가 짝지은 차이의 구간보다 넓다. seed마다 분할과 초기화가 함께 바뀌어 모든 모델이 함께 흔들리기 때문이다. 짝을 짓지 않고 RMSE 열의 "±"끼리 겹치는지로 판단하면 물리 손실의 기여도 불분명해 보인다(12.5장의 짝지은 비교).

연구에서의 의미. ablation은 구성 요소의 기여가 서로 독립이라고 가정한다. 두 요소가 함께 있을 때만 효과가 나는 상호작용이 있으면, 하나씩 빼는 ablation은 각각의 기여를 과소평가하거나 과대평가한다. 구성 요소가 적으면 가능한 조합을 모두 돌리는 요인 설계(factorial design, 23.5장)가 더 정확하다.

부정적 결과

왜 필요한가. 대부분의 아이디어는 작동하지 않는다. 작동하지 않은 결과를 기록하지 않으면 같은 실험을 몇 달 뒤 다시 하거나, 동료가 같은 길을 다시 걷는다. 또 성공한 결과만 남기면 "시도한 것 중 무엇이 통했는가"라는 분모가 사라져, 성공이 얼마나 우연인지 판단할 수 없게 된다(12.4장의 선택 편향).

부정적 결과의 종류. "효과 없음"에도 종류가 있고, 무엇을 배웠는지가 다르다.

결과 구간 배운 것
효과 없음이 확인됨 0을 포함하고 좁다. 의미 있는 크기의 효과를 배제한다 이 조건에서 이 아이디어는 쓸모없다. 강한 지식이다
판정 불가 0을 포함하고 넓다 데이터·seed가 부족하다. 결론을 내리지 않는다
반대 효과 0을 넘어 반대쪽 가설의 메커니즘이 틀렸을 수 있다. 원인 분석의 출발점
구현 실패 학습이 발산했거나 버그 아이디어에 대해서는 아무것도 배우지 않았다

가장 흔한 실수는 둘째 줄(판정 불가)을 첫째 줄(효과 없음)로 기록하는 것이다. 구간이 [−0.30, +0.05]이면 0.30만큼의 큰 개선도, 효과 없음도 데이터와 맞는다. 이 결과로 아이디어를 버리면 실제로 쓸모 있는 아이디어를 버릴 수 있다. 판정 불가는 "seed를 늘리거나 데이터를 늘리면 답할 수 있다"는 기록으로 남긴다.

기록할 것. 부정적 결과에는 성공한 결과보다 더 많은 맥락이 필요하다. 나중에 읽는 사람이 "왜 실패했는가"를 판단해야 하기 때문이다.

  • 가설과 예상한 효과 크기
  • 정확한 설정(커밋, 설정 파일, 데이터 버전 — 23.7장)
  • 결과와 구간, 판정(위 표의 어느 줄인가)
  • 실패 원인에 대한 가설과, 그중 배제한 것과 배제하지 못한 것
  • 어디까지 시도했는가(하이퍼파라미터 범위, seed 수). "학습률 1e-4–1e-2, seed 3개에서 개선 없음"은 "안 됨"보다 훨씬 유용하다

공유. 부정적 결과는 논문 본문에서 한 문단의 한계로, 부록의 "시도했으나 효과가 없었던 변형" 절로, 연구실 위키의 기록으로 공유할 수 있다. 일부 학회는 부정적 결과나 재현 결과를 위한 워크숍·트랙을 운영한다. 투고처의 범위는 해마다 바뀌므로 해당 연도의 모집 공고에서 확인한다.

실습

자신의 실험 결과(없으면 이 장의 시뮬레이션)로 주장–근거 표를 만들고, 대안 설명을 배제한다.

  1. 결과를 보기 전에 정한 조건(데이터 부분집합, 실험 조건)마다 두 방법의 짝지은 차이와 95% 구간을 계산한다. 조건을 결과를 본 뒤 정했다면 그 사실을 표에 적는다.
  2. 오차가 가장 큰 샘플 20개를 직접 열어 보고, 공통 패턴을 한 문장씩 적는다.
  3. 조건별 주장을 동시에 하므로 Holm 보정으로 판정한다. 아래 연습문제의 함수를 쓴다.
  4. 결과에 대한 대안 설명을 세 가지 쓴다(용량, 튜닝 예산, 부수 변경, 누수, 평가 차이 중에서). 각각을 배제하는 추가 실험을 하나씩 설계하고 수행한다.
  5. 이 장의 주장–근거 표 형식으로 정리한다. 근거가 없는 주장은 지우거나 "주장하지 않음" 행으로 옮긴다.

이 장의 시뮬레이션에서 4단계의 결과는 "용량 설명은 부분적으로 맞다(이득의 약 60%)", "attention은 기여를 보이지 못했다"였다. 이처럼 대안 설명을 배제하는 실험은 주장을 줄이는 쪽으로 끝나는 일이 많다. 줄어든 주장이 리뷰를 통과하고 재현되는 주장이다.

이어서 연습문제를 푼다. Holm 절차를 구현하고, 보정된 p-value까지 계산한다.

실패 사례

  • 평균 개선을 모든 조건에서의 개선으로 주장하는 것. 이 장의 예에서 전체 평균은 B가 나았지만 0 °C에서는 A가 나았다. 조건별 표 없이 "B가 낫다"고 쓰면 저온에서 B를 쓰는 독자가 손해를 본다.
  • 평균만 보고 우월을 주장하는 것. seed 3개 평균이 조금 높다는 것은 근거가 아니다. 짝지은 차이와 구간을 보고, 구간이 0을 포함하면 판정을 보류한다.
  • 결과를 본 뒤 조건을 고르는 것. 이긴 조건만 골라 "이 조건에서 B가 낫다"고 쓰면 다중 비교를 숨긴 것이다. 조건은 미리 정하고, 탐색적으로 찾은 조건은 탐색적이라고 쓴다.
  • ablation 없이 이득을 제안한 구성 요소 덕으로 돌리는 것. 용량·튜닝 예산·부수 변경을 맞춘 비교가 없으면 이득의 출처를 모른다.
  • 판정 불가를 효과 없음으로 기록하는 것. 넓은 구간은 무지의 표시이지 부재의 증거가 아니다.

통과 기준

  • 자신의 실험에 대해 주장마다 근거 실험·데이터·조건·지표·구간·판정을 대응시킨 주장–근거 표를 만든다.
  • 결과에 대한 대안 설명을 세 가지 이상 쓰고, 각각을 배제하거나 배제하지 못했음을 실험 결과로 보인다.
  • 부정적 결과 하나(자신의 것, 없으면 이 장의 attention ablation)를 네 종류(효과 없음 확인·판정 불가·반대 효과·구현 실패) 중 하나로 구간과 함께 판정하고, "기록할 것"의 다섯 항목을 채운 기록을 남긴다.
  • 연습문제 holm-correction의 테스트를 통과한다.

참고 문헌

  • Holm (1979). A simple sequentially rejective multiple test procedure. Scandinavian Journal of Statistics, 6(2), 65–70. — 순차 기각으로 Bonferroni보다 검정력이 높은 FWER 제어 절차
  • Lipton, Steinhardt (2018). Troubling trends in machine learning scholarship. arXiv:1807.03341. — 실험적 이득의 출처를 밝히지 않는 문제 등 ML 논문의 흔한 결함