📚 背景知識(読んでから問題へ)
Day161では、ラベル付きを400件・未ラベルを1200件に増やしても、疑似ラベルあり(r>0)が疑似ラベルなし(r=0)を有意に上回る条件は16中0だった。原因として「40件の時点で感情語20語中19.6語が出ていて、未ラベルに新しい手がかりが無い」と推測し、フローチャートにN0.5「未ラベルに、ラベル付きに無い手がかりがあるか」を足した。これは測っていない仮説だった。今日はこれを測る。
疑似ラベルで新しい語を覚える2段階
| 段階 | 何が起きるか | 失敗するとどうなるか |
|---|---|---|
| ① 既知の手がかりで正しく判定する | 未知語 pos117 を含む文書に既知語 pos003 も入っていれば、先生は正しく「ポジ」と判定できる | 既知の手がかりが無い文書は中立語(delivery など)だけで判定され、五分五分の当てずっぽうになる |
| ② その判定が同じ文書の未知語に乗り移る | 再学習で pos117 が正の重みを持つ | ①で誤った文書の未知語は、逆向きの重みを覚える |
mixed データ(ポジ語とネガ語を独立に選び、多い方がラベル)は、1語単位ではこれを満たさない。cv_conf_acc も1seedで7〜24件しか使えないので、この注意がそのまま当てはまる。🗂️ データ・評価スキーマ
| データ/用語 | 値・型 | 役割 |
|---|---|---|
| V | 20 / 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 / test | 40件 / 110件 | Day160と同じ分割。test 1件 = 0.0091 |
| g1 / g5 / z | float | acc(r=1)−acc(r=0) / acc(r=5)−acc(r=0) / g1÷SE。z>+2で有意に得、z<−2で有意に損 |
| nocue_share_r1 | float | round1の採用文書のうち、既知の感情語を1つも含まない文書の割合(診断専用) |
| cv_conf_acc | float | ラベル付き40件だけの5-fold CVで、確信度0.60超の検証文書の正解率(コンペでも測れる) |
📏 評価の仕組み — 勝ち負けと「新しい語の効果」の読み方
判定はDay161と同じく 20seed平均のg1が ±2SE を超えるか。今日はさらに、同じseed・同じ手法で new − seen の差を取り、「未ラベルの中の新しい語」だけの効果を読む。下の図の塗りの丸がnew、白抜きがseen。緑は有意に得、赤は有意に損、灰は区別できない。
❓ 問題
タスクA(分析・予想)
- 感情語を20→200語に増やすと、未ラベルの84%がラベル付きに出ない語を含む。Day161の仮説どおりなら、g1はどうなるはずか
- V=20とV=200のg1を比べるだけでは、差が新しい語のせいだと言えない。V=200でほかに何が変わるかを1つ挙げ、対照群seenで何が切り分けられるかを述べる
- mixedとconsistentで、上の表の①の満たされ方がどう違うかを、Blum & Mitchellの条件(1)の言葉で説明する
タスクB(コーディング)
make_data(n, seed, V, gen)。mixed・V=20でDay160のmake_text_datasetと完全一致swap_to_seen(unl, lab, V, seed)。未知の感情語だけを同じ極性の既知語に置換r1_diagを拡張: 手がかりなし文書の割合・その正解率・手がかりあり文書の正解率・新語の重みの符号正解率(診断専用)cv_conf_acc(lab, seed)(ラベル付き40件だけで測る)- gen 2 × V 2 × pool 2 × D・G × 20seed を回し、表1〜5を出す
- 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(分析)
- Day161の仮説に、今日の実測値を添えて答える
- 新しい語が84%の文書にあるのに得にならなかった理由を、表2の数字を2つ以上使って説明する
- N0.5を書き直す。何を問うか、ラベル付きだけで測れる量は何か、その量がどこまで信用できるか
💡 ヒント
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)
📊 結果の可視化
タスクAの解答例
- 仮説どおりなら、V=200ではg1が正になり+2SEを超えるはず。実測はmixedで −0.0114 / −0.0055、consistentで −0.0682 / −0.0409(有意に負け)
- V=200ではr=0が0.7255→0.5786に下がり(伸びしろが増える)、testの85%に未知語が入る。seenはtest・ラベル付き・r=0・未ラベルの件数・ラベル・語の個数をそろえ、「未ラベル内の未知語の有無」だけを変えるので、new−seenが新語の効果そのものになる
- mixedではポジ語1つを知っていてもネガ語が2つあればネガなので、1語でラベルが決まらない(条件(1)を1語単位で満たさない)。consistentでは既知の感情語が1つあればラベルが決まる
🧭 N0.5の書き直し(タスクC3の解答例)
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 解説
for s in range(3): assert make_data(450, s, 20, "mixed").equals(make_text_dataset(450, s))
| 迷った他案 | 利点 | 採らなかった理由 | 前提が変わったら |
|---|---|---|---|
| 対照群なしでV=20とV=200を比べる | 実験が半分 | r=0が0.15下がり、伸びしろの効果と区別できない | r=0がそろう語彙の作り方ができれば可 |
| 未知語を含む文書を未ラベルから除く | 実装が簡単 | 残る文書が感情語の少ない文書に偏り、件数も変わる | 未ラベルが十分多く語数で層別できれば可 |
| 実データ(IMDb等)で語彙を変える | 本物の長い裾 | 「既知の手がかりがあるか」を真値で数えられない | 仕組みを実データで確かめる段階で本命になる |
_, 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
🧮 数学・統計の補足(文系向け)
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 の学習マップ
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: