Day 162 — NLP系コンペへの応用⑬ — 未ラベルに「新しい語」を与えても疑似ラベルは勝たず、新しい語が1つも無い条件で初めて勝った

2026-09-25 金 / Phase 6(Grandmaster、総括フローチャートの前提検証) 分析→コーディング 語彙2 × 生成2 × 対照群 × 20seed

📚 背景知識(読んでから問題へ)

Day161では、ラベル付きを400件・未ラベルを1200件に増やしても、疑似ラベルあり(r>0)が疑似ラベルなし(r=0)を有意に上回る条件は16中0だった。原因として「40件の時点で感情語20語中19.6語が出ていて、未ラベルに新しい手がかりが無い」と推測し、フローチャートにN0.5「未ラベルに、ラベル付きに無い手がかりがあるか」を足した。これは測っていない仮説だった。今日はこれを測る。

疑似ラベルで新しい語を覚える2段階

段階何が起きるか失敗するとどうなるか
① 既知の手がかりで正しく判定する未知語 pos117 を含む文書に既知語 pos003 も入っていれば、先生は正しく「ポジ」と判定できる既知の手がかりが無い文書は中立語(delivery など)だけで判定され、五分五分の当てずっぽうになる
② その判定が同じ文書の未知語に乗り移る再学習で pos117 が正の重みを持つ①で誤った文書の未知語は、逆向きの重みを覚える
📖
一次資料(2026-09-25参照): Blum & Mitchell “Combining Labeled and Unlabeled Data with Co-Training”(COLT 1998, pp.92–100。拡張版PDF)は、未ラベルを使う条件として (1) どちらの見方も、ラベルが十分あればそれだけで分類できる(アブストラクト: “either view of the example would be sufficient for learning”)、(2) 2つの見方はラベルを条件にすると独立(§5)を置く。上の①は(1)と同じ種類の条件で、Day160と同じ mixed データ(ポジ語とネガ語を独立に選び、多い方がラベル)は、1語単位ではこれを満たさない。
⚠️
Oliver et al. 2018(arXiv:1804.09170)§4.6: 小さい検証セットでは手法の比較が信頼できない。今日の cv_conf_acc も1seedで7〜24件しか使えないので、この注意がそのまま当てはまる。

🗂️ データ・評価スキーマ

データ/用語値・型役割
V20 / 200感情語の語彙数。20はDay151〜161と同じ語、200は pos000〜neg099
gen="mixed"生成方法Day160と同じ。ポジ0〜2語・ネガ0〜2語を独立に選び、多い方がラベル(V=20でDay160と完全一致をassert)
gen="consistent"生成方法先に極性を決め、感情語1〜3語をすべてその極性から選ぶ(実レビューに近い)
pool="new"未ラベル300件そのまま。V=200では84%の文書がラベル付きに無い感情語を含む
pool="seen"未ラベル300件対照群。未知の感情語だけを同じ極性の既知語に置換。文書数・ラベル・語の個数・中立語は同じ
lab / test40件 / 110件Day160と同じ分割。test 1件 = 0.0091
g1 / g5 / zfloatacc(r=1)−acc(r=0) / acc(r=5)−acc(r=0) / g1÷SE。z>+2で有意に得、z<−2で有意に損
nocue_share_r1floatround1の採用文書のうち、既知の感情語を1つも含まない文書の割合(診断専用)
cv_conf_accfloatラベル付き40件だけの5-fold CVで、確信度0.60超の検証文書の正解率(コンペでも測れる)

📏 評価の仕組み — 勝ち負けと「新しい語の効果」の読み方

判定はDay161と同じく 20seed平均のg1が ±2SE を超えるか。今日はさらに、同じseed・同じ手法で new − seen の差を取り、「未ラベルの中の新しい語」だけの効果を読む。下の図の塗りの丸がnew、白抜きがseen。緑は有意に得、赤は有意に損、灰は区別できない。

g1 = acc(r=1) − acc(r=0)、20seed平均 ± 2SE(●new / ○seen対照群) −0.12 −0.08 −0.04 0 +0.04 1件 consistent V=20 D +0.0264 (z +5.2) consistent V=20 G +0.0286 (z +5.8) consistent V=200 D -0.0682 (z -7.3) consistent V=200 G -0.0409 (z -3.4) mixed V=20 D -0.0923 (z -7.8) mixed V=20 G -0.0591 (z -5.7) mixed V=200 D -0.0114 (z -1.4) mixed V=200 G -0.0055 (z -0.6)
💡
対で引く理由: consistent V=200・Dのnew・seenのSEは0.0094・0.0086。別々のseedなら差のSEは約0.0127で、差 −0.0218 は −1.7SE(区別できない)。同じseedで引くとSEは0.0083に下がり、−2.6SE(有意)として読める。

❓ 問題

タスクA(分析・予想)

  1. 感情語を20→200語に増やすと、未ラベルの84%がラベル付きに出ない語を含む。Day161の仮説どおりなら、g1はどうなるはずか
  2. V=20とV=200のg1を比べるだけでは、差が新しい語のせいだと言えない。V=200でほかに何が変わるかを1つ挙げ、対照群seenで何が切り分けられるかを述べる
  3. mixedとconsistentで、上の表の①の満たされ方がどう違うかを、Blum & Mitchellの条件(1)の言葉で説明する

タスクB(コーディング)

  1. make_data(n, seed, V, gen)。mixed・V=20でDay160の make_text_dataset と完全一致
  2. swap_to_seen(unl, lab, V, seed)。未知の感情語だけを同じ極性の既知語に置換
  3. r1_diag を拡張: 手がかりなし文書の割合・その正解率・手がかりあり文書の正解率・新語の重みの符号正解率(診断専用)
  4. cv_conf_acc(lab, seed)(ラベル付き40件だけで測る)
  5. gen 2 × V 2 × pool 2 × D・G × 20seed を回し、表1〜5を出す
  6. assert 6個: (a) Day161再現 (b) mixed V=200 newで+2SEを超えない (c) mixedでnew−seenが+2SEを超えない (d) consistent V=20でD・Gとも+2SE超え (e) consistent V=200 newでD・Gとも−2SE未満 (f) V=200 newで採用の3割超が手がかりなし・正解率0.4〜0.6

タスクC(分析)

  1. Day161の仮説に、今日の実測値を添えて答える
  2. 新しい語が84%の文書にあるのに得にならなかった理由を、表2の数字を2つ以上使って説明する
  3. N0.5を書き直す。何を問うか、ラベル付きだけで測れる量は何か、その量がどこまで信用できるか

💡 ヒント

ヒント1方向性
「新しい語がある」と「新しい語を正しく覚えられる」は別。新しい語の重みは、その語を含む文書に付いた疑似ラベルから決まる。感情語を1つも知らない文書でも、中立語の重みだけで確信度0.60を超えることはありうる。V=200ではr=0自体も下がるので、V以外をそろえた対照群が要る。
ヒント2アプローチ
mixed側は乱数の呼び出し順(choice→choice→sample(ポジ)→sample(ネガ)→sample(中立)→randint)を崩さない。置換では同じ語が文書に2回入らないよう使用済みを除く。対照群との比較は、同じseed・同じ手法の対で差を取ってからSEを出す。
ヒント3コード骨格
def swap_to_seen(unl, lab, V, seed):
    pos_w, neg_w = vocab(V)
    kp, kn = known_sent(lab, V)                 # ラベル付きに出た感情語
    rng = random.Random(seed + 4000)
    out = []
    for t in unl["text"]:
        ws, used = t.split(), set(t.split())
        for i, w in enumerate(ws):
            pool = kp if w in pos_w else kn if w in neg_w else None
            if pool is None or w in pool:       # 中立語・既知語はそのまま
                continue
            ws[i] = rng.choice([c for c in pool if c not in used] or pool)
            used.add(ws[i])
        out.append(" ".join(ws))
    return unl.assign(text=out)

# 対での差
p = e.pivot_table(index=["gen", "V", "method", "seed"], columns="pool", values="g1")
d = (p["new"] - p["seen"]).groupby(["gen", "V", "method"])
print(d.mean(), d.std(ddof=1) / np.sqrt(20))

✅ 模範解答(全文)

前提: Day160模範解答の import〜day157_val_es を day160_lib.py として保存しておく(Day161と同じ)。

import random
import time
from day160_lib import *   # Day160 模範解答の make_text_dataset 〜 day157_val_es をそのまま保存したもの


def vocab(V):
    """感情語をV語(ポジV/2・ネガV/2)にする。V=20ならDay151〜161と同じ語彙。"""
    if V == 20:
        return POSITIVE_WORDS, NEGATIVE_WORDS
    return [f"pos{i:03d}" for i in range(V // 2)], [f"neg{i:03d}" for i in range(V // 2)]


def make_data(n, seed, V, gen):
    """gen="mixed": Day160と同じ(ポジ0〜2語・ネガ0〜2語を独立に選び、多い方がラベル)。
    gen="consistent": 文書の極性を先に決め、感情語1〜3語をすべてその極性から選ぶ(実レビューに近い)。"""
    pos_w, neg_w = vocab(V)
    rng = random.Random(seed)
    rows = []
    for _ in range(n):
        if gen == "mixed":
            n_pos, n_neg = rng.choice([0, 1, 2]), rng.choice([0, 1, 2])
            if n_pos == 0 and n_neg == 0:
                n_pos = 1
            sent, y = rng.sample(pos_w, k=n_pos) + rng.sample(neg_w, k=n_neg), int(n_pos > n_neg)
        else:
            y = int(rng.random() < 0.5)
            sent = rng.sample(pos_w if y else neg_w, k=rng.choice([1, 2, 3]))
        words = sent + rng.sample(NEUTRAL_WORDS, k=rng.randint(3, 6))
        rng.shuffle(words)
        rows.append({"text": " ".join(words), "label": y})
    return pd.DataFrame(rows)


def split_v(seed, V, gen):
    """Day160のsplit_seedと同じ分割(ラベル付き40/未ラベル300/test110)。"""
    df = make_data(450, seed, V, gen)
    rest, test = train_test_split(df, test_size=110, random_state=seed, stratify=df["label"])
    lab, unl = train_test_split(rest, train_size=40, random_state=seed, stratify=rest["label"])
    return (d.reset_index(drop=True) for d in (lab, unl, test))


def known_sent(lab, V):
    """ラベル付きに1回以上出た感情語(ポジ・ネガ別)。"""
    pos_w, neg_w = vocab(V)
    toks = set(" ".join(lab["text"]).split())
    return [w for w in pos_w if w in toks], [w for w in neg_w if w in toks]


def swap_to_seen(unl, lab, V, seed):
    """対照群: 未ラベルの各文書で、ラベル付きに出ない感情語だけを同じ極性の既知語に置き換える。
    文書数・ラベル・感情語の個数・中立語はそのまま。変わるのは「新しい語があるか」だけ。"""
    pos_w, neg_w = vocab(V)
    kp, kn = known_sent(lab, V)
    rng = random.Random(seed + 4000)
    out = []
    for t in unl["text"]:
        ws, used = t.split(), set(t.split())
        for i, w in enumerate(ws):
            pool = kp if w in pos_w else kn if w in neg_w else None
            if pool is None or w in pool:
                continue
            ws[i] = rng.choice([c for c in pool if c not in used] or pool)
            used.add(ws[i])
        out.append(" ".join(ws))
    return unl.assign(text=out)


def cue_flags(df, lab, V):
    """(未知の感情語を含むか, 既知の感情語を1つも含まないか) を文書ごとに返す。"""
    pos_w, neg_w = vocab(V)
    sent = set(pos_w) | set(neg_w)
    known = set(sum(known_sent(lab, V), []))
    toks = df["text"].str.split()
    has_new = toks.map(lambda ws: any(w in sent and w not in known for w in ws)).to_numpy()
    no_known = toks.map(lambda ws: not any(w in known for w in ws)).to_numpy()
    return has_new, no_known


def r1_diag(lab, unl, fn, seed, V, threshold=0.60, n_clusters=8):
    """診断専用(未ラベルの真ラベルを使う。選択には使わない)。round1で採用された文書について、
    件数・正解率・既知の感情語を含まない文書の割合とその正解率・round1後に新語の重みの符号が合っている割合。"""
    vec, model = fit(lab["text"], lab["label"])
    proba = model.predict_proba(vec.transform(unl["text"]))[:, 1]
    n_added = int(((proba >= threshold) | (proba <= 1 - threshold)).sum())
    mask, pl = fn(model, vec, unl, n_added, n_clusters, seed)
    y = unl.loc[mask, "label"].to_numpy()
    _, no_known = cue_flags(unl.loc[mask], lab, V)
    vec2, m2 = fit(list(lab["text"]) + unl.loc[mask, "text"].tolist(), list(lab["label"]) + pl.tolist())
    coef = dict(zip(vec2.get_feature_names_out(), m2.coef_[0]))
    pos_w, neg_w = vocab(V)
    kp, kn = known_sent(lab, V)
    signs = [coef[w] > 0 for w in pos_w if w not in kp and w in coef] + \
            [coef[w] < 0 for w in neg_w if w not in kn and w in coef]
    return {"n_added_r1": n_added, "pl_acc_r1": float((pl == y).mean()),
            "nocue_share_r1": float(no_known.mean()),
            "pl_acc_nocue": float((pl[no_known] == y[no_known]).mean()) if no_known.any() else np.nan,
            "pl_acc_cue": float((pl[~no_known] == y[~no_known]).mean()) if (~no_known).any() else np.nan,
            "n_new_learned": len(signs), "new_sign_acc": float(np.mean(signs)) if signs else np.nan}


def cv_conf_acc(lab, seed, threshold=0.60):
    """ラベル付き40件だけで測れる量: 5-fold CVで、確信度が閾値を超えた検証文書の正解率。
    疑似ラベルの正解率(本来は見えない)の代わりになるかを確かめる。"""
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=seed)
    hit = []
    for tr, va in skf.split(lab["text"], lab["label"]):
        vec, m = fit(lab["text"].iloc[tr], lab["label"].iloc[tr])
        p = m.predict_proba(vec.transform(lab["text"].iloc[va]))[:, 1]
        sel = (p >= threshold) | (p <= 1 - threshold)
        hit += list(((p >= 0.5).astype(int) == lab["label"].iloc[va].to_numpy())[sel])
    return float(np.mean(hit)) if hit else np.nan, len(hit)


if __name__ == "__main__":
    for s in range(3):   # mixed・V=20 の生成が Day160 と完全に同じか
        assert make_data(450, s, 20, "mixed").equals(make_text_dataset(450, s))
    METHODS = {"D": select_D, "G": select_G}
    t0 = time.time()
    rows, diag = [], []
    for gen in ["mixed", "consistent"]:
        for V in [20, 200]:
            for s in range(20):
                lab, unl_new, test = split_v(s, V, gen)
                unl_seen = swap_to_seen(unl_new, lab, V, s)
                t_new, _ = cue_flags(test, lab, V)
                u_new, u_nocue = cue_flags(unl_new, lab, V)
                ca, cn = cv_conf_acc(lab, s)
                kp, kn = known_sent(lab, V)
                vec0, m0 = fit(lab["text"], lab["label"])
                a0 = pd.Series(np.abs(m0.coef_[0]), index=vec0.get_feature_names_out())
                is_neu = a0.index.isin(NEUTRAL_WORDS)
                diag.append({"gen": gen, "V": V, "seed": s, "known_sent": len(kp) + len(kn),
                             "test_new_share": t_new.mean(), "unl_new_share": u_new.mean(),
                             "unl_seen_new_share": cue_flags(unl_seen, lab, V)[0].mean(),
                             "unl_nocue_share": u_nocue.mean(), "cv_conf_acc": ca, "cv_conf_n": cn,
                             "coef_sent_med": a0[~is_neu].median(), "coef_neu_med": a0[is_neu].median()})
                evals = {"test": test}
                if t_new.any():
                    evals["new"] = test[t_new]
                for pool, unl in [("new", unl_new), ("seen", unl_seen)]:
                    for name, fn in METHODS.items():
                        c = ipl_curve(lab["text"], lab["label"], unl, fn, s, evals, n_rounds=5)
                        rows.append({"gen": gen, "V": V, "pool": pool, "method": name, "seed": s,
                                     "r0": c["test"][0], "g1": c["test"][1] - c["test"][0],
                                     "g5": c["test"][5] - c["test"][0],
                                     "g1_newdocs": c["new"][1] - c["new"][0] if t_new.any() else np.nan,
                                     **r1_diag(lab, unl, fn, s, V)})
            print(f"{gen} V={V} done ({time.time() - t0:.0f}s)", flush=True)
    e, d = pd.DataFrame(rows), pd.DataFrame(diag)
    e.to_csv("day162_res.csv", index=False)
    d.to_csv("day162_diag.csv", index=False)

    K = ["gen", "V", "pool", "method"]
    g = e.groupby(K)
    t = g[["r0", "g1", "g5", "g1_newdocs"]].mean()
    t["g1_se"] = g["g1"].std(ddof=1) / np.sqrt(20)
    t["g5_se"] = g["g5"].std(ddof=1) / np.sqrt(20)
    t["g1_z"], t["g5_z"] = t["g1"] / t["g1_se"], t["g5"] / t["g5_se"]
    t["win"] = g["g1"].apply(lambda x: int((x > 0).sum()))
    t["loss"] = g["g1"].apply(lambda x: int((x < 0).sum()))
    r1 = g[["n_added_r1", "pl_acc_r1", "nocue_share_r1", "pl_acc_nocue", "pl_acc_cue", "n_new_learned", "new_sign_acc"]].mean()
    p = e.pivot_table(index=["gen", "V", "method", "seed"], columns="pool", values="g1")
    dd = (p["new"] - p["seen"]).rename("d_g1").reset_index().groupby(["gen", "V", "method"])["d_g1"]
    td = pd.DataFrame({"d_g1(new-seen)": dd.mean(), "se": dd.std(ddof=1) / np.sqrt(20)})

    pd.set_option("display.width", 250); pd.set_option("display.float_format", lambda x: f"{x:.4f}")
    print("[表1: 20seed, g1=r1-r0, g5=r5-r0, z=平均/SE]"); print(t.to_string()); print()
    print("[表2: round1で採用された文書の中身(診断専用)]"); print(r1.to_string()); print()
    print("[表3: 同じseed・同じ手法での 新語あり − 新語なし(対照) のg1差]"); print(td.to_string()); print()
    print("[表4: データの性質とラベル付きだけで測れる量]")
    print(d.groupby(["gen", "V"]).mean(numeric_only=True).drop(columns="seed").to_string()); print()
    u = e[e["pool"] == "new"].groupby(["gen", "V", "seed"])["g1"].mean().rename("g1_DG").reset_index().merge(d, on=["gen", "V", "seed"])
    rho_all = u["cv_conf_acc"].corr(u["g1_DG"], method="spearman")
    rho_in = u.groupby(["gen", "V"]).apply(lambda x: x["cv_conf_acc"].corr(x["g1_DG"], method="spearman"), include_groups=False)
    print(f"[表5] cv_conf_acc と g1(D・G平均) の順位相関: 80単位全体 {rho_all:.3f} / 条件内 {rho_in.round(3).to_dict()}"); print()

    assert abs(t.loc[("mixed", 20, "new", "D"), "g1"] + 0.0923) < 1e-3                     # (a) Day161を再現
    assert abs(t.loc[("mixed", 20, "new", "G"), "g1"] + 0.0591) < 1e-3
    m200 = t.loc[("mixed", 200, "new")]
    assert (m200["g1_z"] < 2).all() and (m200["g5_z"] < 2).all()                          # (b) 語彙200でも有意に勝たない
    assert (td.loc["mixed", "d_g1(new-seen)"] < 2 * td.loc["mixed", "se"]).all()          # (c) 新語の有無で有意差なし
    assert (t.loc[("consistent", 20, "new"), "g1_z"] > 2).all()                           # (d) 新語ゼロでも勝つ条件がある
    assert (t.loc[("consistent", 200, "new"), "g1_z"] < -2).all()                         # (e) 新語だらけでも有意に負ける
    nc = r1.loc[(slice(None), 200, "new"), :]
    assert (nc["nocue_share_r1"] > 0.3).all() and (nc["pl_acc_nocue"].between(0.4, 0.6)).all()  # (f) 採用の3割超が手がかりなしで、正解率は五分五分
    print(f"[OK] 6個のassertすべて成立  ({time.time() - t0:.1f}s)")

実測結果(2026-09-25、Python 3.13.12 / scikit-learn 1.9.1 / pandas 3.0.6 / numpy 2.5.3、壁時計50秒)

mixed V=20 done (11s)
mixed V=200 done (24s)
consistent V=20 done (35s)
consistent V=200 done (50s)
[表1: 20seed, g1=r1-r0, g5=r5-r0, z=平均/SE]
                               r0      g1      g5  g1_newdocs  g1_se  g5_se    g1_z     g5_z  win  loss
gen        V   pool method                                                                             
consistent 20  new  D      0.9664  0.0264  0.0327      0.0333 0.0051 0.0048  5.1223   6.7906   15     0
                    G      0.9664  0.0286  0.0314      0.0333 0.0049 0.0049  5.8937   6.4431   17     1
               seen D      0.9664  0.0295  0.0332      0.0000 0.0049 0.0048  6.0475   6.8930   16     0
                    G      0.9664  0.0291  0.0314      0.0333 0.0047 0.0049  6.2009   6.4431   17     0
           200 new  D      0.6541 -0.0682 -0.1318     -0.0556 0.0094 0.0094 -7.2470 -14.0453    0    19
                    G      0.6541 -0.0409 -0.0823     -0.0375 0.0120 0.0124 -3.4068  -6.6351    2    17
               seen D      0.6541 -0.0464 -0.0805     -0.0429 0.0086 0.0152 -5.4092  -5.2975    1    18
                    G      0.6541  0.0132  0.0050      0.0143 0.0136 0.0118  0.9678   0.4235   11     8
mixed      20  new  D      0.7255 -0.0923 -0.1559     -0.0232 0.0119 0.0126 -7.7455 -12.4029    0    18
                    G      0.7255 -0.0591 -0.0836     -0.0261 0.0104 0.0099 -5.6609  -8.4806    2    18
               seen D      0.7255 -0.0968 -0.1564     -0.0550 0.0122 0.0123 -7.9349 -12.7348    0    19
                    G      0.7255 -0.0686 -0.0895      0.0185 0.0111 0.0110 -6.1811  -8.1245    1    19
           200 new  D      0.5786 -0.0114 -0.0223      0.0114 0.0084 0.0086 -1.3460  -2.5865    7    13
                    G      0.5786 -0.0055 -0.0150      0.0083 0.0092 0.0093 -0.5917  -1.6152    8     8
               seen D      0.5786 -0.0186 -0.0227      0.0072 0.0082 0.0090 -2.2735  -2.5301    3    14
                    G      0.5786 -0.0077 -0.0164      0.0102 0.0077 0.0090 -1.0000  -1.8163    7    10

[表2: round1で採用された文書の中身(診断専用)]
                            n_added_r1  pl_acc_r1  nocue_share_r1  pl_acc_nocue  pl_acc_cue  n_new_learned  new_sign_acc
gen        V   pool method                                                                                              
consistent 20  new  D         204.0000     0.9928          0.0000           NaN      0.9928         0.1500        1.0000
                    G         204.0000     0.9524          0.0031        0.4611      0.9537         0.1500        1.0000
               seen D         205.2000     0.9921          0.0000           NaN      0.9921         0.0000           NaN
                    G         205.2000     0.9544          0.0000           NaN      0.9544         0.0000           NaN
           200 new  D          59.9500     0.7846          0.4116        0.5395      0.9679        55.6500        0.7093
                    G          59.9500     0.5382          0.4799        0.4952      0.5790        59.6000        0.5350
               seen D          85.1000     0.9764          0.0000           NaN      0.9764         0.0000           NaN
                    G          85.1000     0.7306          0.0000           NaN      0.7306         0.0000           NaN
mixed      20  new  D         107.1500     0.8442          0.0022        0.3333      0.8450         0.4000        0.6667
                    G         107.1500     0.5813          0.0035        0.3750      0.5822         0.4000        0.5000
               seen D         109.1000     0.8489          0.0000           NaN      0.8489         0.0000           NaN
                    G         109.1000     0.5900          0.0000           NaN      0.5900         0.0000           NaN
           200 new  D          80.9500     0.6426          0.4548        0.5288      0.7382        73.9000        0.5109
                    G          80.9500     0.5145          0.4750        0.4994      0.5254        73.3000        0.4853
               seen D          89.1000     0.7440          0.0000           NaN      0.7440         0.0000           NaN
                    G          89.1000     0.5214          0.0000           NaN      0.5214         0.0000           NaN

[表3: 同じseed・同じ手法での 新語あり − 新語なし(対照) のg1差]
                       d_g1(new-seen)     se
gen        V   method                       
consistent 20  D              -0.0032 0.0022
               G              -0.0005 0.0005
           200 D              -0.0218 0.0083
               G              -0.0541 0.0120
mixed      20  D               0.0045 0.0033
               G               0.0095 0.0063
           200 D               0.0073 0.0048
               G               0.0023 0.0078

[表4: データの性質とラベル付きだけで測れる量]
                known_sent  test_new_share  unl_new_share  unl_seen_new_share  unl_nocue_share  cv_conf_acc  cv_conf_n  coef_sent_med  coef_neu_med
gen        V                                                                                                                                       
consistent 20      19.8500          0.0114         0.0155              0.0000           0.0022       0.9979    23.7500         0.5929        0.1949
           200     65.9000          0.8518         0.8425              0.0000           0.4690       0.7118     7.3000         0.2214        0.2129
mixed      20      19.6000          0.0359         0.0385              0.0000           0.0038       0.8348    13.2000         0.4069        0.2276
           200     67.5500          0.8350         0.8430              0.0000           0.4523       0.5286     9.7500         0.2077        0.2185

[表5] cv_conf_acc と g1(D・G平均) の順位相関: 80単位全体 0.324 / 条件内 {('consistent', 20): 0.06, ('consistent', 200): 0.224, ('mixed', 20): 0.051, ('mixed', 200): 0.407}

[OK] 6個のassertすべて成立  (49.8s)
❌
発見1(仮説は外れた): mixed V=200では未ラベルの84.3%に新語があるのに、g1はD −0.0114(z −1.3)、G −0.0055(z −0.6)。対照群との差もD +0.0073(SE 0.0048)、G +0.0023(SE 0.0078)で2SE未満。
✅
発見2(新語が無いのに勝った): consistent V=20(新語を含む未ラベルは1.6%)で g1 D +0.0264(z +5.1、15勝0敗)、G +0.0286(z +5.9、17勝1敗)。Day151から続くシリーズで初めての有意な勝ち。対照群も +0.0295 / +0.0291 で同じなので、勝因は新語ではない。
❌
発見3(新語がかえって害になる): consistent V=200では new − seen が D −0.0218(−2.6SE)、G −0.0541(−4.5SE)。新語を含む未ラベルの方が有意に悪い。
🔍
発見4(分かれ目): V=200・newでは採用文書の41〜48%が既知の感情語を含まず、その疑似ラベル正解率は0.4952〜0.5395。先生の係数の大きさ(中央値)は感情語0.2077〜0.2214、中立語0.2129〜0.2185で区別がついていない(V=20 consistentでは0.5929対0.1949)。新語の重みの符号正解率はmixed Dで0.5109、Gで0.4853。覚えたのではなく、でたらめな向きの重みを足しただけだった。
⚠️
発見5(ラベル付きだけで測れる量の強さと弱さ): cv_conf_accは4条件の平均では勝ち負けと順序が合う(0.998のみ勝ち、0.835・0.712は負け、0.529は差なし)。しかしseed単位の順位相関は全体0.324、条件内0.051〜0.407と弱い。

📊 結果の可視化

D・new・round1の採用文書:既知の感情語あり(青)/なし(赤)と、それぞれの疑似ラベル正解率 consistent V=20 あり 100%・正解率 0.993 g1 +0.0264 勝ち mixed V=20 あり 100%・正解率 0.845 g1 −0.0923 負け consistent V=200 あり 59%・正解率 0.968 なし 41%・0.539 g1 −0.0682 負け mixed V=200 あり 55%・正解率 0.738 なし 45%・0.529 g1 −0.0114 差なし 「なし」の正解率は0.5前後=コイントス。先生は中立語だけで確信度0.60を超えている(診断専用: 真のラベルを使用)
cv_conf_acc(ラベル付き40件のCVだけで測れる)と結果 — 軸は0.5〜1.0 consistent V=20 0.998(23.75件) 勝ち z+5.1/+5.9 mixed V=20 0.835(13.2件) 負け z−7.7/−5.7 consistent V=200 0.712(7.3件) 負け z−7.2/−3.4 mixed V=200 0.529(9.75件) 差なし z−1.3/−0.6 仮の目安 0.95 (件数)は1seedでしきい値0.60を超えた検証文書の数。seed単位の条件内順位相関は0.05〜0.41と弱い

タスクAの解答例

  1. 仮説どおりなら、V=200ではg1が正になり+2SEを超えるはず。実測はmixedで −0.0114 / −0.0055、consistentで −0.0682 / −0.0409(有意に負け)
  2. V=200ではr=0が0.7255→0.5786に下がり(伸びしろが増える)、testの85%に未知語が入る。seenはtest・ラベル付き・r=0・未ラベルの件数・ラベル・語の個数をそろえ、「未ラベル内の未知語の有無」だけを変えるので、new−seenが新語の効果そのものになる
  3. mixedではポジ語1つを知っていてもネガ語が2つあればネガなので、1語でラベルが決まらない(条件(1)を1語単位で満たさない)。consistentでは既知の感情語が1つあればラベルが決まる

🧭 N0.5の書き直し(タスクC3の解答例)

N0.5(撤回)未ラベルに、ラベル付きに無い手がかりがあるか 新語が84%の文書にあっても勝たず(mixed V=200: z −1.3/−0.6)、 新語がほぼ無い条件で勝った(consistent V=20: z +5.1/+5.9) N0.5'(追加)しきい値を超えた文書は、ほぼ全部正しいか 測り方: ラベル付きだけの5-fold CVで、確信度0.60超の検証文書の正解率(cv_conf_acc) 根拠: 勝ったのは0.998の条件だけ。0.835では有意に負けた(Day162、4条件) 注意: 0.95は4条件から置いた仮の値。seed単位の条件内順位相関 0.05〜0.41 0.95未満 0.95以上 超えた件数20未満 疑似ラベルは使わない 0.835でも g1 −0.0923 (mixed V=20・D) N1へ r=0を候補に入れて CVで比較(Day161) この量は読めない seed・分割を変えてCVを 重ね、件数を増やす 未解決の残差(Day163): consistent V=200・seen・Dは疑似ラベル正解率0.9764なのに g1 −0.0464(z −5.4)。 同じ条件のGは +0.0132。N0.5'を通っても、確信度順の選び方(D)が負ける経路が残っている可能性がある

C1: 仮説は外れた。新語が84%の文書にあっても勝たず、consistent V=200では新語がある方が有意に悪く、新語がほぼ無いconsistent V=20で初めて勝った。C2: V=200・newでは採用の41〜48%が手がかりなし文書で正解率0.50〜0.54。先生は感情語と中立語を重みの大きさで区別できず(中央値0.21前後で同じ)、中立語だけで確信度0.60を超える。新語は「覚える材料」ではなく「でたらめなラベルが付く文書を増やす原因」になった。

🪜 Step-by-Step 解説

1
V=20ではDay160と同じデータになるようにする。 生成関数を書き直すと乱数の使い方が1か所ずれただけで別データになり、「Vの効果」と「データが変わった効果」が混ざる。assert (a) で20seedのg1がDay161と小数第4位まで一致した。
for s in range(3):
    assert make_data(450, s, 20, "mixed").equals(make_text_dataset(450, s))
2
新しい語「だけ」を消した対照群を作る。 V=200ではr=0が0.15下がるので、V=20との比較は「新語」と「伸びしろ」が混ざる。seenは未知語を同じ極性の既知語に置換するので、ラベルも語の個数も変わらない。
迷った他案利点採らなかった理由前提が変わったら
対照群なしでV=20とV=200を比べる実験が半分r=0が0.15下がり、伸びしろの効果と区別できないr=0がそろう語彙の作り方ができれば可
未知語を含む文書を未ラベルから除く実装が簡単残る文書が感情語の少ない文書に偏り、件数も変わる未ラベルが十分多く語数で層別できれば可
実データ(IMDb等)で語彙を変える本物の長い裾「既知の手がかりがあるか」を真値で数えられない仕組みを実データで確かめる段階で本命になる
3
採用された文書の中身を真のラベルで診断する。 手がかりあり(consistent・D 0.9679)となし(0.5395)で正解率が大きく違う。確信度0.60は確信度の出どころを見ていない。
_, no_known = cue_flags(unl.loc[mask], lab, V)       # 既知の感情語を1つも含まない文書
pl_acc_nocue = (pl[no_known] == y[no_known]).mean()   # V=200・new で 0.4952〜0.5395
4
ラベル付きだけで測れる量を探し、その弱さも測る。 手がかりなし文書の割合はコンペでは測れない。代わりにCVで「しきい値を超えた文書は本当に当たっているか」を測る。条件平均では勝った条件だけが0.998と飛び抜けたが、1seed 7〜24件なので条件内ρは0.05〜0.41。使える量を見つけたことと、1回のCVで信用できることは別なので両方を残す。

🧮 数学・統計の補足(文系向け)

🏫
転校生のたとえ: 知らない転校生(新しい語)がいつも山田くん(既知のポジ語)と一緒にいれば、先生は「明るい子だろう」と推測できる(consistent)。mixedでは転校生はいつも明るい子と暗い子の両方と一緒にいて、1人を知っていても雰囲気は決まらない。V=200では転校生の半分は知っている友達が1人もいない集団にいて、先生は「今日は給食がカレーだから(中立語)明るいだろう」と当てにならない理由で判断している。

test 1件の重さ: 110件で1件=0.0091。consistent V=20の g1 +0.0264 は約2.9問を正解に変えた(r=0の誤答は約3.7問)。consistent V=200・Gの対照群との差 −0.0541 は約6問を不正解に変えた。

🏆 Kaggleでの実践的な使い方

  • 「testにしか出ない語が多いから疑似ラベルが効くはず」と考えない。 新語が84%の文書にあっても勝たず、新語の方が有意に悪い条件もあった
  • 疑似ラベルの前に「しきい値を超えたOOF予測の正解率」を測る。 追加の学習は不要。今日の4条件では0.998だけが勝ち、0.835は負けた。数万件のラベル付きがあるコンペなら今日の40件より何桁も多い件数で読める(今日は確かめていない)
  • 確信度の出どころを1回は覗く。 線形モデルなら係数、GBDTなら重要度で、確信度を作る特徴が本物の手がかりか、たまたまラベルと相関した頻出特徴かを見る。V=200では感情語と中立語の係数の大きさがほぼ同じ(0.21前後)だった

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
未ラベルに新しい情報があれば疑似ラベルは効くよくある説明で、Day161の仮説もこれだった新語が84%にあっても勝たなかった。新語を覚えるには、その文書の判定が既知の本物の手がかりに支えられている必要がある
疑似ラベル正解率0.84あれば十分8割当たれば良くなりそうに見えるmixed V=20・Dは0.8442でg1 −0.0923(z −7.7)。勝ったのは0.99台だけ
V=20とV=200のg1の差=新語の効果変えたのはVだけに見えるr=0が0.7255→0.5786に下がる。対照群との差を見る
確信度0.60超=根拠を持った判定確信度は1つの数に見えるV=200では採用の41〜48%が既知の感情語ゼロで、正解率0.50前後
cv_conf_accを1回のCVで0.95と比べる条件平均ではきれいに分かれた1seed 7〜24件。条件内ρ 0.05〜0.41。回数を重ねてから読む

🚧 扱わなかった範囲と理由

  • 未ラベル1200件・ラベル80件以上: Day161で「害が縮むだけ」と分かっているので40/300/110に固定。予備実行(mixed、同じ20seed)ではV=200・未ラベル1200件でも g1 D −0.0132・G −0.0009 で+2SEは超えなかった
  • V=60: 予備実行でmixed・未ラベル300件の g1 D −0.0764・G −0.0577。V=20とV=200の間に収まり新しい情報が無いので本番から外した
  • しきい値0.60・クラスタ数8: 固定。予備実行(consistentの生成が本番と少し違う版・8seed・クラスタ選択なし)では、V=200でしきい値0.70にすると採用が1seed約1.4件まで減り、効果自体が消えた
  • Yarowsky(1995): consistentの設定に近いが、ACL AnthologyのPDFがスキャン画像で本文を確認できなかったので根拠として引用しない
  • 実データ: 語と極性の関係を真値で数えるため合成データで測った。実データでは中立語もラベルと弱く相関するので、41〜48%などの数値はそのまま当てはまらない

🚀 次のステップ

  • 発展: 文書内の感情語が同じ極性である確率を0.5/0.7/0.9/1.0と連続に振り、勝ち負けが入れ替わる点とそのときのcv_conf_accを対応させて、仮の目安0.95を決め直す
  • 次回予告: Day163では残差を調べる。consistent V=200・seen・Dは疑似ラベル正解率0.9764なのに g1 −0.0464(z −5.4)、同条件のGは +0.0132。「確信度順に選ぶと、中立語がたまたまラベルと相関した文書ばかり集まり、中立語の見かけの重みを強める」(Day151の選択バイアスの再来)という仮説を、round1前後の中立語係数の変化で確かめる

Phase 6 の学習マップ

1-23
Phase6主要テーマ・総復習(Day126-149、完了)
発展①〜⑬
NLP系コンペへの応用(Day162: N0.5「新しい手がかり」仮説の検証→撤回)

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: