Skip to content

Commit 3110b52

Browse files
authored
Merge pull request #273 from lovit/feature/267
refactor: PMI 함수를 soynlp.word에서 soynlp.utils로 이동 (#267)
2 parents 3f8ab86 + 71a4b52 commit 3110b52

4 files changed

Lines changed: 122 additions & 64 deletions

File tree

soynlp/utils/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from soynlp.core.lrgraph import LRGraph
22

33
from .math import svd
4+
from .pmi import pmi
45
from .utils import (
56
CorpusLoader,
67
EojeolCounter,
@@ -23,4 +24,6 @@
2324
"LRGraph",
2425
# math
2526
"svd",
27+
# pmi
28+
"pmi",
2629
]

soynlp/utils/pmi.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import Any
2+
3+
import numpy as np
4+
from scipy.sparse import csr_matrix, dia_matrix, diags
5+
6+
7+
def _as_diag(px: np.ndarray, alpha: float) -> dia_matrix:
8+
px_diag: dia_matrix = diags(px.tolist()[0])
9+
px_diag.data[0] = np.asarray([0 if v == 0 else 1 / (v + alpha) for v in px_diag.data[0]])
10+
return px_diag
11+
12+
13+
def _logarithm_and_ppmi(exp_pmi: Any, min_exp_pmi: float) -> csr_matrix:
14+
n, m = exp_pmi.shape
15+
16+
rows, cols = exp_pmi.nonzero()
17+
data = exp_pmi.data
18+
19+
indices = np.where(data >= min_exp_pmi)[0]
20+
rows = rows[indices]
21+
cols = cols[indices]
22+
data = data[indices]
23+
24+
data = np.log(data)
25+
exp_pmi_ = csr_matrix((data, (rows, cols)), shape=(n, m))
26+
return exp_pmi_
27+
28+
29+
def pmi(
30+
X: csr_matrix,
31+
py: np.ndarray | None = None,
32+
min_pmi: float = 0,
33+
alpha: float = 0.0,
34+
beta: float = 1,
35+
) -> tuple[csr_matrix, np.ndarray, np.ndarray]:
36+
"""Transform `X` to Positive-PMI matrix (CSR sparse matrix)
37+
38+
Args:
39+
X (scipy.sparse.csr_matrix) :
40+
shape = (n items, n features)
41+
py (numpy.ndarray, optional) :
42+
shape = (1, word), probability of context words.
43+
If `py` is None, `pmi` function uses normalized row sum of `X`
44+
min_pmi (float) :
45+
Minimum value of pmi.
46+
alpha (float) :
47+
Smoothing factor. Default is `0.0`
48+
beta (float) :
49+
Smoothing factor. Default is `1.0`
50+
51+
Returns:
52+
pmi (scipy.sparse.csr_matrix)
53+
px (numpy.ndarray)
54+
py (numpy.ndarray)
55+
"""
56+
57+
assert 0 < beta <= 1
58+
59+
px = np.asarray((X.sum(axis=1) / X.sum()).reshape(-1))
60+
pxy = X / X.sum()
61+
if py is None:
62+
py = np.asarray((X.sum(axis=0) / X.sum()).reshape(-1))
63+
py_arr: np.ndarray = py
64+
if beta < 1:
65+
py_arr = py_arr**beta
66+
py_arr /= py_arr.sum()
67+
assert py_arr.shape[1] == pxy.shape[1] # type: ignore[index]
68+
69+
px_diag = _as_diag(px, 0)
70+
py_diag = _as_diag(py_arr, alpha)
71+
exp_pmi = px_diag.dot(pxy).dot(py_diag)
72+
73+
min_exp_pmi = 1 if min_pmi == 0 else np.exp(min_pmi)
74+
pmi_mat = _logarithm_and_ppmi(exp_pmi, min_exp_pmi)
75+
76+
return pmi_mat, px, py_arr

soynlp/word/pmi.py

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
1-
from typing import Any
2-
3-
import numpy as np
4-
from scipy.sparse import csr_matrix, dia_matrix, diags
5-
6-
7-
def _as_diag(px: np.ndarray, alpha: float) -> dia_matrix:
8-
px_diag: dia_matrix = diags(px.tolist()[0])
9-
px_diag.data[0] = np.asarray([0 if v == 0 else 1 / (v + alpha) for v in px_diag.data[0]])
10-
return px_diag
1+
"""Deprecated: soynlp.word.pmi → soynlp.utils.pmi로 이동되었습니다."""
112

3+
import warnings
124

13-
def _logarithm_and_ppmi(exp_pmi: Any, min_exp_pmi: float) -> csr_matrix:
14-
n, m = exp_pmi.shape
15-
16-
rows, cols = exp_pmi.nonzero()
17-
data = exp_pmi.data
5+
import numpy as np
6+
from scipy.sparse import csr_matrix
187

19-
indices = np.where(data >= min_exp_pmi)[0]
20-
rows = rows[indices]
21-
cols = cols[indices]
22-
data = data[indices]
8+
from soynlp.utils.pmi import _as_diag, _logarithm_and_ppmi
9+
from soynlp.utils.pmi import pmi as _pmi_impl
2310

24-
data = np.log(data)
25-
exp_pmi_ = csr_matrix((data, (rows, cols)), shape=(n, m))
26-
return exp_pmi_
11+
__all__ = ["pmi", "_as_diag", "_logarithm_and_ppmi"]
2712

2813

2914
def pmi(
@@ -33,44 +18,10 @@ def pmi(
3318
alpha: float = 0.0,
3419
beta: float = 1,
3520
) -> tuple[csr_matrix, np.ndarray, np.ndarray]:
36-
"""Transform `X` to Positive-PMI matrix (CSR sparse matrix)
37-
38-
Args:
39-
X (scipy.sparse.csr_matrix) :
40-
shape = (n items, n features)
41-
py (numpy.ndarray, optional) :
42-
shape = (1, word), probability of context words.
43-
If `py` is None, `pmi` function uses normalized row sum of `X`
44-
min_pmi (float) :
45-
Minimum value of pmi.
46-
alpha (float) :
47-
Smoothing factor. Default is `0.0`
48-
beta (float) :
49-
Smoothing factor. Default is `1.0`
50-
51-
Returns:
52-
pmi (scipy.sparse.csr_matrix)
53-
px (numpy.ndarray)
54-
py (numpy.ndarray)
55-
"""
56-
57-
assert 0 < beta <= 1
58-
59-
px = np.asarray((X.sum(axis=1) / X.sum()).reshape(-1))
60-
pxy = X / X.sum()
61-
if py is None:
62-
py = np.asarray((X.sum(axis=0) / X.sum()).reshape(-1))
63-
py_arr: np.ndarray = py
64-
if beta < 1:
65-
py_arr = py_arr**beta
66-
py_arr /= py_arr.sum()
67-
assert py_arr.shape[1] == pxy.shape[1] # type: ignore[index]
68-
69-
px_diag = _as_diag(px, 0)
70-
py_diag = _as_diag(py_arr, alpha)
71-
exp_pmi = px_diag.dot(pxy).dot(py_diag)
72-
73-
min_exp_pmi = 1 if min_pmi == 0 else np.exp(min_pmi)
74-
pmi_mat = _logarithm_and_ppmi(exp_pmi, min_exp_pmi)
75-
76-
return pmi_mat, px, py_arr
21+
""".. deprecated:: soynlp.utils.pmi를 사용하세요."""
22+
warnings.warn(
23+
"soynlp.word.pmi is deprecated. Use soynlp.utils.pmi instead.",
24+
DeprecationWarning,
25+
stacklevel=2,
26+
)
27+
return _pmi_impl(X, py=py, min_pmi=min_pmi, alpha=alpha, beta=beta)

tests/unit/test_pmi.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import warnings
2+
13
import numpy as np
24
from scipy.sparse import csr_matrix
35

4-
from soynlp.word import pmi
6+
from soynlp.utils import pmi
57

68

79
def test_pmi():
@@ -19,3 +21,29 @@ def test_pmi():
1921
print(f"\nPMI: \n{pmi_mat.todense()}")
2022
print(f"\nPx: {px}")
2123
print(f"\nPy: {py}")
24+
25+
26+
def test_pmi_import_from_utils():
27+
"""soynlp.utils.pmi가 정상적으로 import되고 동작한다."""
28+
from soynlp.utils.pmi import pmi as pmi_direct
29+
30+
x = csr_matrix(np.ones((3, 4)))
31+
pmi_mat, px, py = pmi_direct(x)
32+
assert pmi_mat.shape == (3, 4)
33+
34+
35+
def test_pmi_deprecated_word_import():
36+
"""soynlp.word.pmi는 DeprecationWarning을 발생시키며 동일한 결과를 반환한다."""
37+
from soynlp.word.pmi import pmi as pmi_word
38+
39+
x = csr_matrix(np.ones((3, 4)))
40+
with warnings.catch_warnings(record=True) as w:
41+
warnings.simplefilter("always")
42+
result = pmi_word(x)
43+
assert len(w) == 1
44+
assert issubclass(w[0].category, DeprecationWarning)
45+
assert "soynlp.utils.pmi" in str(w[0].message)
46+
47+
# 결과는 새 경로와 동일해야 함
48+
expected = pmi(x)
49+
assert (result[0] - expected[0]).nnz == 0

0 commit comments

Comments
 (0)